release-skill 0.9.27 → 0.9.28
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/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +15 -9
- package/README.zh-CN.md +15 -9
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +2620 -1674
- package/adapters/claude/skills/release-assess/SKILL.md +64 -1
- package/adapters/claude/skills/release-finish/SKILL.md +2 -0
- package/adapters/claude/skills/release-help/SKILL.md +2 -2
- package/adapters/claude/skills/release-setup/SKILL.md +5 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +2620 -1674
- package/adapters/codex/skills/release-assess/SKILL.md +64 -1
- package/adapters/codex/skills/release-finish/SKILL.md +2 -0
- package/adapters/codex/skills/release-help/SKILL.md +2 -2
- package/adapters/codex/skills/release-setup/SKILL.md +5 -2
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/consumer-contract-vectors.json +7 -7
- package/adapters/cursor/bin/foundation-resource-binding.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +2620 -1674
- package/adapters/cursor/skills/release-assess/SKILL.md +64 -1
- package/adapters/cursor/skills/release-finish/SKILL.md +2 -0
- package/adapters/cursor/skills/release-help/SKILL.md +2 -2
- package/adapters/cursor/skills/release-setup/SKILL.md +5 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2620 -1674
- package/adapters/kimi/skills/release-assess/SKILL.md +64 -1
- package/adapters/kimi/skills/release-finish/SKILL.md +2 -0
- package/adapters/kimi/skills/release-help/SKILL.md +2 -2
- package/adapters/kimi/skills/release-setup/SKILL.md +5 -2
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/consumer-contract-vectors.json +7 -7
- package/adapters/qoder/bin/foundation-resource-binding.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +2620 -1674
- package/adapters/qoder/skills/release-assess/SKILL.md +64 -1
- package/adapters/qoder/skills/release-finish/SKILL.md +2 -0
- package/adapters/qoder/skills/release-help/SKILL.md +2 -2
- package/adapters/qoder/skills/release-setup/SKILL.md +5 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +2620 -1674
- package/adapters/workbuddy/skills/release-assess/SKILL.md +64 -1
- package/adapters/workbuddy/skills/release-finish/SKILL.md +2 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-setup/SKILL.md +5 -2
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/release-skill-cli.mjs +45 -1
- package/bin/release-skill.bundle.mjs +2620 -1674
- package/package.json +4 -4
- package/platform-manifest.json +4 -4
- package/skills/release-assess/SKILL.md +64 -1
- package/skills/release-finish/SKILL.md +2 -0
- package/skills/release-help/SKILL.md +2 -2
- package/skills/release-setup/SKILL.md +5 -2
- package/skills-src/release-assess/SKILL.md +64 -1
- package/skills-src/release-finish/SKILL.md +2 -0
- package/skills-src/release-help/SKILL.md +2 -2
- package/skills-src/release-setup/SKILL.md +5 -2
- package/src/commands/post-release-local.mjs +23 -88
- package/src/commands/setup.mjs +149 -6
- package/src/core/adoption-assessment.mjs +502 -4
- package/src/core/local-host-observation.mjs +225 -0
- package/src/platforms/registry.mjs +12 -4
- package/src/producers/foundation-resource-projection.mjs +3 -3
|
@@ -32,11 +32,14 @@
|
|
|
32
32
|
* timestamps, findings are sorted by (category, code, fieldPath, unitId),
|
|
33
33
|
* and `assessmentDigest` binds the config digest plus the full discovery
|
|
34
34
|
* facts so any fact change invalidates earlier suggestion summaries for any
|
|
35
|
-
* future write path.
|
|
35
|
+
* future write path. When host-coverage diagnosis is requested, the digest
|
|
36
|
+
* also binds this invocation's complete JSON input, live observation records,
|
|
37
|
+
* and load-error fact.
|
|
36
38
|
*
|
|
37
39
|
* @module adoption-assessment
|
|
38
40
|
*/
|
|
39
41
|
|
|
42
|
+
import { getPlatform, normalizeHostId } from '../platforms/registry.mjs';
|
|
40
43
|
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
41
44
|
import { resolveProducerVersion } from './evidence.mjs';
|
|
42
45
|
|
|
@@ -290,6 +293,482 @@ export function deriveStatus(findings) {
|
|
|
290
293
|
return ASSESSMENT_STATUS.ADOPTED;
|
|
291
294
|
}
|
|
292
295
|
|
|
296
|
+
const HOST_COVERAGE_OBSERVATION_STATES = Object.freeze(['installed', 'not-installed', 'unknown']);
|
|
297
|
+
|
|
298
|
+
export function emptyHostCoverage() {
|
|
299
|
+
return Object.freeze({
|
|
300
|
+
requested: false,
|
|
301
|
+
complete: false,
|
|
302
|
+
results: Object.freeze([]),
|
|
303
|
+
findings: Object.freeze([]),
|
|
304
|
+
binding: null,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function publicHostCoverage(coverage = emptyHostCoverage()) {
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
requested: coverage.requested === true,
|
|
311
|
+
complete: coverage.complete === true,
|
|
312
|
+
results: coverage.results ?? Object.freeze([]),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function platformIdOrNull(value) {
|
|
317
|
+
try {
|
|
318
|
+
return getPlatform(value).id;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function canonicalizeHost(value) {
|
|
325
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
326
|
+
return { ok: false, raw: value, reason: 'host id is missing' };
|
|
327
|
+
}
|
|
328
|
+
const trimmed = value.trim();
|
|
329
|
+
let foundationId = null;
|
|
330
|
+
try {
|
|
331
|
+
foundationId = await normalizeHostId(trimmed);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const platformId = platformIdOrNull(trimmed);
|
|
334
|
+
if (platformId) return { ok: true, raw: value, host: platformId };
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
raw: value,
|
|
338
|
+
reason: error?.message ?? 'host id could not be normalized',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const foundationPlatform = platformIdOrNull(foundationId);
|
|
342
|
+
if (foundationPlatform) return { ok: true, raw: value, host: foundationPlatform };
|
|
343
|
+
const rawPlatform = platformIdOrNull(trimmed);
|
|
344
|
+
if (rawPlatform) return { ok: true, raw: value, host: rawPlatform };
|
|
345
|
+
return { ok: true, raw: value, host: foundationId };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function uniquePush(list, value) {
|
|
349
|
+
if (!list.includes(value)) list.push(value);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function applicableDistribution(unit, host) {
|
|
353
|
+
let distributionType = null;
|
|
354
|
+
try {
|
|
355
|
+
distributionType = getPlatform(host).distributionType;
|
|
356
|
+
} catch {
|
|
357
|
+
distributionType = null;
|
|
358
|
+
}
|
|
359
|
+
if (distributionType == null) return null;
|
|
360
|
+
return (unit?.distributions ?? []).find((item) => item.type === distributionType) ?? null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function nonemptyString(value) {
|
|
364
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function declaredPluginIdentity(unit, host) {
|
|
368
|
+
const localPlugin = nonemptyString(unit?.postPublish?.localHostUpdate?.plugin);
|
|
369
|
+
if (localPlugin) return localPlugin;
|
|
370
|
+
return nonemptyString(applicableDistribution(unit, host)?.plugin);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function declaredMarketplaceIdentity(unit, host) {
|
|
374
|
+
const localMarketplace = nonemptyString(unit?.postPublish?.localHostUpdate?.hub?.name);
|
|
375
|
+
if (localMarketplace) return localMarketplace;
|
|
376
|
+
return nonemptyString(applicableDistribution(unit, host)?.marketplace);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function wiringFacts(config, unitId, host) {
|
|
380
|
+
if (!config) return { available: false };
|
|
381
|
+
const unit = (config.releaseUnits ?? []).find((item) => item.id === unitId);
|
|
382
|
+
if (!unit) return { available: false, unitFound: false };
|
|
383
|
+
let distributionType = null;
|
|
384
|
+
try {
|
|
385
|
+
distributionType = getPlatform(host).distributionType;
|
|
386
|
+
} catch {
|
|
387
|
+
distributionType = null;
|
|
388
|
+
}
|
|
389
|
+
const hasLocalUpdate = Array.isArray(unit.postPublish?.localHostUpdate?.hosts)
|
|
390
|
+
&& unit.postPublish.localHostUpdate.hosts.includes(host);
|
|
391
|
+
if (distributionType == null) {
|
|
392
|
+
return { available: false, unitFound: true, hasLocalUpdate };
|
|
393
|
+
}
|
|
394
|
+
const hasDistribution = (unit.distributions ?? []).some((item) => item.type === distributionType);
|
|
395
|
+
return {
|
|
396
|
+
available: true,
|
|
397
|
+
unitFound: true,
|
|
398
|
+
hasDistribution,
|
|
399
|
+
hasLocalUpdate,
|
|
400
|
+
distributionType,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function observationKey(unitId, host) {
|
|
405
|
+
return `${unitId}\u0000${host}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validObservedAt(value) {
|
|
409
|
+
const observedAt = nonemptyString(value);
|
|
410
|
+
if (observedAt == null || Number.isNaN(Date.parse(observedAt))) return null;
|
|
411
|
+
return observedAt;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeObservationRecord(observation, host) {
|
|
415
|
+
return {
|
|
416
|
+
host,
|
|
417
|
+
plugin: nonemptyString(observation?.plugin),
|
|
418
|
+
marketplace: nonemptyString(observation?.marketplace),
|
|
419
|
+
state: HOST_COVERAGE_OBSERVATION_STATES.includes(observation?.state) ? observation.state : null,
|
|
420
|
+
source: nonemptyString(observation?.source),
|
|
421
|
+
observedAt: validObservedAt(observation?.observedAt),
|
|
422
|
+
version: nonemptyString(observation?.version),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function observationFactsIssue(observation) {
|
|
427
|
+
if (!observation || observation.state == null) return 'install observation is missing';
|
|
428
|
+
if (observation.plugin == null) return 'observation plugin is missing';
|
|
429
|
+
if (observation.marketplace == null) return 'observation marketplace is missing';
|
|
430
|
+
if (observation.source == null) return 'observation source is missing';
|
|
431
|
+
if (observation.observedAt == null) return 'observation observedAt is missing or not a valid timestamp';
|
|
432
|
+
if (observation.state === 'installed' && observation.version == null) {
|
|
433
|
+
return 'installed observation version is missing';
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function mergeObservation(fileObservation, liveObservation) {
|
|
439
|
+
if (!fileObservation && !liveObservation) return null;
|
|
440
|
+
if (!liveObservation) return fileObservation;
|
|
441
|
+
if (!fileObservation) return liveObservation;
|
|
442
|
+
const identityConflict = (
|
|
443
|
+
(fileObservation.plugin && liveObservation.plugin && fileObservation.plugin !== liveObservation.plugin)
|
|
444
|
+
|| (fileObservation.marketplace && liveObservation.marketplace && fileObservation.marketplace !== liveObservation.marketplace)
|
|
445
|
+
);
|
|
446
|
+
if (identityConflict) {
|
|
447
|
+
return {
|
|
448
|
+
...liveObservation,
|
|
449
|
+
state: 'unknown',
|
|
450
|
+
reason: 'file and live observations disagree on plugin identity',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (
|
|
454
|
+
fileObservation.state
|
|
455
|
+
&& fileObservation.state !== 'unknown'
|
|
456
|
+
&& liveObservation.state
|
|
457
|
+
&& liveObservation.state !== 'unknown'
|
|
458
|
+
&& fileObservation.state !== liveObservation.state
|
|
459
|
+
) {
|
|
460
|
+
return {
|
|
461
|
+
...liveObservation,
|
|
462
|
+
state: 'unknown',
|
|
463
|
+
reason: 'file and live observations disagree on install state',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return liveObservation;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Compare explicit host-coverage input with project wiring and observations.
|
|
471
|
+
* Support facts come only from the input declaration, never from distributions
|
|
472
|
+
* or localHostUpdate.
|
|
473
|
+
*
|
|
474
|
+
* @param {object} options
|
|
475
|
+
* @returns {Promise<object>}
|
|
476
|
+
*/
|
|
477
|
+
export async function evaluateHostCoverage({
|
|
478
|
+
requested = false,
|
|
479
|
+
input = null,
|
|
480
|
+
loadError = null,
|
|
481
|
+
config = null,
|
|
482
|
+
liveObservations = [],
|
|
483
|
+
} = {}) {
|
|
484
|
+
if (!requested) return emptyHostCoverage();
|
|
485
|
+
|
|
486
|
+
const issues = [];
|
|
487
|
+
const results = [];
|
|
488
|
+
const binding = { input, liveObservations, loadError };
|
|
489
|
+
if (loadError) issues.push(loadError);
|
|
490
|
+
if (input == null) {
|
|
491
|
+
return finishHostCoverage({ requested: true, complete: false, results, issues, binding });
|
|
492
|
+
}
|
|
493
|
+
if (input.schemaVersion !== 1) issues.push('schemaVersion must be 1');
|
|
494
|
+
if (!Array.isArray(input.units)) {
|
|
495
|
+
issues.push('units must be an array');
|
|
496
|
+
return finishHostCoverage({
|
|
497
|
+
requested: true,
|
|
498
|
+
complete: false,
|
|
499
|
+
results,
|
|
500
|
+
issues,
|
|
501
|
+
binding,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const seenUnitIds = new Set();
|
|
506
|
+
for (const unit of input.units) {
|
|
507
|
+
const unitId = typeof unit?.unitId === 'string' ? unit.unitId : null;
|
|
508
|
+
if (unitId == null || unitId.length === 0) {
|
|
509
|
+
issues.push('unitId is missing');
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const duplicatedUnit = seenUnitIds.has(unitId);
|
|
513
|
+
if (duplicatedUnit) {
|
|
514
|
+
issues.push(`unitId "${unitId}" is duplicated`);
|
|
515
|
+
} else {
|
|
516
|
+
seenUnitIds.add(unitId);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const supportSource = typeof unit?.support?.source === 'string' ? unit.support.source.trim() : '';
|
|
520
|
+
if (supportSource.length === 0) issues.push(`unit "${unitId}" is missing support.source`);
|
|
521
|
+
|
|
522
|
+
const supportHosts = [];
|
|
523
|
+
for (const raw of Array.isArray(unit?.support?.hosts) ? unit.support.hosts : []) {
|
|
524
|
+
const canonical = await canonicalizeHost(raw);
|
|
525
|
+
if (!canonical.ok) {
|
|
526
|
+
issues.push(`unit "${unitId}" has unresolvable support host ${JSON.stringify(raw)}`);
|
|
527
|
+
if (typeof raw === 'string' && raw.length > 0) uniquePush(supportHosts, raw);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
uniquePush(supportHosts, canonical.host);
|
|
531
|
+
}
|
|
532
|
+
if (!Array.isArray(unit?.support?.hosts)) issues.push(`unit "${unitId}" is missing support.hosts`);
|
|
533
|
+
|
|
534
|
+
const includeHosts = [];
|
|
535
|
+
if (!Array.isArray(unit?.delivery?.include)) {
|
|
536
|
+
issues.push(`unit "${unitId}" is missing delivery.include`);
|
|
537
|
+
} else {
|
|
538
|
+
for (const raw of unit.delivery.include) {
|
|
539
|
+
const canonical = await canonicalizeHost(raw);
|
|
540
|
+
if (!canonical.ok) {
|
|
541
|
+
issues.push(`unit "${unitId}" has unresolvable include host ${JSON.stringify(raw)}`);
|
|
542
|
+
if (typeof raw === 'string' && raw.length > 0) uniquePush(includeHosts, raw);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
uniquePush(includeHosts, canonical.host);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const excludeByHost = new Map();
|
|
550
|
+
if (!Array.isArray(unit?.delivery?.exclude)) {
|
|
551
|
+
issues.push(`unit "${unitId}" is missing delivery.exclude`);
|
|
552
|
+
} else {
|
|
553
|
+
for (const entry of unit.delivery.exclude) {
|
|
554
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.reason !== 'string' || entry.reason.length === 0) {
|
|
555
|
+
issues.push(`unit "${unitId}" exclude entries must be {host, reason} objects`);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const canonical = await canonicalizeHost(entry.host);
|
|
559
|
+
if (!canonical.ok) {
|
|
560
|
+
issues.push(`unit "${unitId}" has unresolvable exclude host ${JSON.stringify(entry.host)}`);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (excludeByHost.has(canonical.host)) {
|
|
564
|
+
issues.push(`unit "${unitId}" host "${canonical.host}" is excluded more than once`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
excludeByHost.set(canonical.host, entry.reason);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const installIfMissing = [];
|
|
572
|
+
if (!Array.isArray(unit?.delivery?.installIfMissing)) {
|
|
573
|
+
issues.push(`unit "${unitId}" is missing delivery.installIfMissing`);
|
|
574
|
+
} else {
|
|
575
|
+
for (const raw of unit.delivery.installIfMissing) {
|
|
576
|
+
const canonical = await canonicalizeHost(raw);
|
|
577
|
+
if (!canonical.ok) {
|
|
578
|
+
issues.push(`unit "${unitId}" has unresolvable installIfMissing host ${JSON.stringify(raw)}`);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
uniquePush(installIfMissing, canonical.host);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const fileObservations = new Map();
|
|
586
|
+
const duplicateHosts = new Set();
|
|
587
|
+
if (!Array.isArray(unit?.observations)) {
|
|
588
|
+
issues.push(`unit "${unitId}" is missing observations`);
|
|
589
|
+
} else {
|
|
590
|
+
for (const observation of unit.observations) {
|
|
591
|
+
const canonical = await canonicalizeHost(observation?.host);
|
|
592
|
+
if (!canonical.ok) {
|
|
593
|
+
issues.push(`unit "${unitId}" has unresolvable observation host ${JSON.stringify(observation?.host)}`);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const key = observationKey(unitId, canonical.host);
|
|
597
|
+
if (fileObservations.has(key) || duplicateHosts.has(canonical.host)) {
|
|
598
|
+
duplicateHosts.add(canonical.host);
|
|
599
|
+
fileObservations.delete(key);
|
|
600
|
+
issues.push(`unit "${unitId}" has duplicate observations for host "${canonical.host}"`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const normalized = normalizeObservationRecord(observation, canonical.host);
|
|
604
|
+
if (normalized.state == null) {
|
|
605
|
+
issues.push(`unit "${unitId}" observation for "${canonical.host}" has invalid state`);
|
|
606
|
+
}
|
|
607
|
+
fileObservations.set(key, normalized);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const liveByHost = new Map();
|
|
612
|
+
for (const observation of liveObservations) {
|
|
613
|
+
if (observation?.unitId !== unitId) continue;
|
|
614
|
+
const canonical = await canonicalizeHost(observation.host);
|
|
615
|
+
if (!canonical.ok) continue;
|
|
616
|
+
liveByHost.set(canonical.host, {
|
|
617
|
+
...normalizeObservationRecord(observation, canonical.host),
|
|
618
|
+
reason: observation.reason,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const declaredHosts = [];
|
|
623
|
+
for (const host of [...supportHosts, ...includeHosts, ...excludeByHost.keys(), ...installIfMissing]) {
|
|
624
|
+
uniquePush(declaredHosts, host);
|
|
625
|
+
}
|
|
626
|
+
for (const observation of [...fileObservations.values(), ...liveByHost.values()]) {
|
|
627
|
+
uniquePush(declaredHosts, observation.host);
|
|
628
|
+
}
|
|
629
|
+
declaredHosts.sort((a, b) => a.localeCompare(b));
|
|
630
|
+
|
|
631
|
+
const unitConfig = (config?.releaseUnits ?? []).find((item) => item.id === unitId) ?? null;
|
|
632
|
+
if (config != null && unitConfig == null) {
|
|
633
|
+
issues.push(`requested unit "${unitId}" is not declared in config.releaseUnits`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
for (const host of declaredHosts) {
|
|
637
|
+
const excluded = excludeByHost.has(host);
|
|
638
|
+
const included = includeHosts.includes(host);
|
|
639
|
+
const supported = supportHosts.includes(host);
|
|
640
|
+
const firstInstall = installIfMissing.includes(host);
|
|
641
|
+
const fileObservation = fileObservations.get(observationKey(unitId, host));
|
|
642
|
+
const liveObservation = liveByHost.get(host);
|
|
643
|
+
const observation = mergeObservation(fileObservation, liveObservation);
|
|
644
|
+
const duplicate = duplicateHosts.has(host);
|
|
645
|
+
const wiring = wiringFacts(config, unitId, host);
|
|
646
|
+
const expectedPlugin = declaredPluginIdentity(unitConfig, host);
|
|
647
|
+
const expectedMarketplace = declaredMarketplaceIdentity(unitConfig, host);
|
|
648
|
+
const identityConflict = Boolean(
|
|
649
|
+
(observation?.plugin && expectedPlugin && observation.plugin !== expectedPlugin)
|
|
650
|
+
|| (observation?.marketplace && expectedMarketplace && observation.marketplace !== expectedMarketplace),
|
|
651
|
+
);
|
|
652
|
+
const factsIssue = observationFactsIssue(observation);
|
|
653
|
+
|
|
654
|
+
let status = 'unknown';
|
|
655
|
+
let reason = 'host coverage facts are incomplete';
|
|
656
|
+
if (duplicate) {
|
|
657
|
+
reason = 'duplicate observations for the same host';
|
|
658
|
+
} else if (config != null && unitConfig == null) {
|
|
659
|
+
reason = 'requested unit is not declared in config.releaseUnits';
|
|
660
|
+
} else if (excluded && included) {
|
|
661
|
+
reason = 'host is both included and excluded';
|
|
662
|
+
issues.push(`unit "${unitId}" host "${host}" is both included and excluded`);
|
|
663
|
+
} else if (excluded) {
|
|
664
|
+
status = FINDING_CATEGORY.NOT_APPLICABLE;
|
|
665
|
+
reason = `已排除:${excludeByHost.get(host)}`;
|
|
666
|
+
} else if (supportSource.length === 0) {
|
|
667
|
+
reason = 'support.source is missing';
|
|
668
|
+
} else if (!supported) {
|
|
669
|
+
reason = 'host is not declared in support.hosts; support cannot be inferred from wiring';
|
|
670
|
+
} else if (!included) {
|
|
671
|
+
reason = 'supported host is not included and has no exclusion reason';
|
|
672
|
+
} else if (identityConflict) {
|
|
673
|
+
reason = 'observation identity conflicts with the unit distribution or local-update identity';
|
|
674
|
+
} else if (factsIssue) {
|
|
675
|
+
reason = factsIssue;
|
|
676
|
+
} else if (observation.state === 'unknown') {
|
|
677
|
+
reason = observation.reason ?? 'install observation is unknown';
|
|
678
|
+
} else if (observation.state === 'not-installed' && !firstInstall) {
|
|
679
|
+
status = FINDING_CATEGORY.NOT_APPLICABLE;
|
|
680
|
+
reason = 'host is not installed and this round has no first-install duty';
|
|
681
|
+
} else if (!wiring.available) {
|
|
682
|
+
reason = 'project wiring facts are unavailable';
|
|
683
|
+
} else if (!wiring.hasDistribution || !wiring.hasLocalUpdate) {
|
|
684
|
+
status = FINDING_CATEGORY.MANDATORY_GAP;
|
|
685
|
+
const missing = [
|
|
686
|
+
wiring.hasDistribution ? null : 'applicable distribution',
|
|
687
|
+
wiring.hasLocalUpdate ? null : 'localHostUpdate host wiring',
|
|
688
|
+
].filter(Boolean);
|
|
689
|
+
reason = `supported in-scope host is missing ${missing.join(' and ')}`;
|
|
690
|
+
} else {
|
|
691
|
+
status = FINDING_CATEGORY.SATISFIED;
|
|
692
|
+
reason = observation.state === 'not-installed'
|
|
693
|
+
? 'first-install duty has complete wiring'
|
|
694
|
+
: 'supported in-scope host is installed and wired';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!duplicatedUnit && status === 'unknown') {
|
|
698
|
+
issues.push(`unit "${unitId}" host "${host}": ${reason}`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const evidence = {
|
|
702
|
+
support: { hosts: [...supportHosts], source: supportSource || null },
|
|
703
|
+
delivery: {
|
|
704
|
+
include: [...includeHosts],
|
|
705
|
+
exclude: [...excludeByHost.entries()].map(([excludeHost, excludeReason]) => ({
|
|
706
|
+
host: excludeHost,
|
|
707
|
+
reason: excludeReason,
|
|
708
|
+
})),
|
|
709
|
+
installIfMissing: [...installIfMissing],
|
|
710
|
+
},
|
|
711
|
+
observation: observation ?? null,
|
|
712
|
+
wiring,
|
|
713
|
+
};
|
|
714
|
+
if (!duplicatedUnit) {
|
|
715
|
+
results.push(Object.freeze({
|
|
716
|
+
unitId,
|
|
717
|
+
host,
|
|
718
|
+
status,
|
|
719
|
+
reason,
|
|
720
|
+
evidence,
|
|
721
|
+
}));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
results.sort((a, b) => a.unitId.localeCompare(b.unitId) || a.host.localeCompare(b.host));
|
|
727
|
+
const complete = issues.length === 0 && results.every((item) => item.status !== 'unknown');
|
|
728
|
+
return finishHostCoverage({
|
|
729
|
+
requested: true,
|
|
730
|
+
complete,
|
|
731
|
+
results,
|
|
732
|
+
issues,
|
|
733
|
+
binding,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function finishHostCoverage({ requested, complete, results, issues, binding }) {
|
|
738
|
+
const findings = [];
|
|
739
|
+
if (requested && !complete) {
|
|
740
|
+
findings.push(createFinding({
|
|
741
|
+
category: FINDING_CATEGORY.MANDATORY_GAP,
|
|
742
|
+
code: 'HOST_COVERAGE_FACTS_MISSING',
|
|
743
|
+
fieldPath: 'hostCoverage',
|
|
744
|
+
message: `宿主覆盖诊断已请求,但完成本次检查所需事实缺失:${issues[0] ?? 'unknown facts remain'}`,
|
|
745
|
+
evidence: { issues, complete: false },
|
|
746
|
+
action: '补齐支持出处、范围、观察或接线事实后重新评估;未知不得写成未安装或已通过。',
|
|
747
|
+
severity: 'blocking',
|
|
748
|
+
}));
|
|
749
|
+
}
|
|
750
|
+
for (const result of results) {
|
|
751
|
+
if (result.status !== FINDING_CATEGORY.MANDATORY_GAP) continue;
|
|
752
|
+
findings.push(createFinding({
|
|
753
|
+
category: FINDING_CATEGORY.MANDATORY_GAP,
|
|
754
|
+
code: 'HOST_COVERAGE_WIRING_GAP',
|
|
755
|
+
unitId: result.unitId,
|
|
756
|
+
fieldPath: `hostCoverage.${result.unitId}.${result.host}`,
|
|
757
|
+
message: `发布单元 "${result.unitId}" 宿主 "${result.host}" 覆盖不足:${result.reason}`,
|
|
758
|
+
evidence: result.evidence,
|
|
759
|
+
action: '补齐适用分发或 localHostUpdate 宿主接线后重新评估;安装观察不能证明加载成功。',
|
|
760
|
+
severity: 'blocking',
|
|
761
|
+
}));
|
|
762
|
+
}
|
|
763
|
+
return Object.freeze({
|
|
764
|
+
requested,
|
|
765
|
+
complete,
|
|
766
|
+
results: Object.freeze(results),
|
|
767
|
+
findings: Object.freeze(findings),
|
|
768
|
+
binding,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
293
772
|
/**
|
|
294
773
|
* Derive gate suggestions from setup's gate candidates (scenarios 5 & 6).
|
|
295
774
|
*
|
|
@@ -588,6 +1067,7 @@ const STATUS_LABEL = Object.freeze({
|
|
|
588
1067
|
* @param {Array<Object>} options.gateDiagnostics - Non-actionable discovered
|
|
589
1068
|
* scripts retained for explicit diagnosis without affecting status/summary.
|
|
590
1069
|
* @param {string} [options.next] - Precise next step.
|
|
1070
|
+
* @param {Object} [options.hostCoverage] - Host-coverage diagnosis result.
|
|
591
1071
|
* @returns {Object} Frozen report.
|
|
592
1072
|
*/
|
|
593
1073
|
export function buildAssessmentReport({
|
|
@@ -600,6 +1080,7 @@ export function buildAssessmentReport({
|
|
|
600
1080
|
gateSuggestions,
|
|
601
1081
|
gateDiagnostics = [],
|
|
602
1082
|
next = null,
|
|
1083
|
+
hostCoverage = emptyHostCoverage(),
|
|
603
1084
|
}) {
|
|
604
1085
|
const sortedFindings = [...findings].sort((a, b) => (
|
|
605
1086
|
(CATEGORY_RANK[a.category] ?? 9) - (CATEGORY_RANK[b.category] ?? 9)
|
|
@@ -642,13 +1123,17 @@ export function buildAssessmentReport({
|
|
|
642
1123
|
note: '评估默认离线,未观测远端;不能据此否定本地接入,也不代表具备生产发布条件。',
|
|
643
1124
|
}];
|
|
644
1125
|
|
|
1126
|
+
const digestPayload = hostCoverage.requested
|
|
1127
|
+
? { configDigest, discoveryFacts: facts, hostCoverage: hostCoverage.binding }
|
|
1128
|
+
: { configDigest, discoveryFacts: facts };
|
|
1129
|
+
const publicCoverage = publicHostCoverage(hostCoverage);
|
|
645
1130
|
const report = {
|
|
646
1131
|
command: 'release-setup',
|
|
647
1132
|
mode: 'adoption-assessment',
|
|
648
1133
|
status,
|
|
649
1134
|
configPath,
|
|
650
1135
|
configDigest,
|
|
651
|
-
assessmentDigest: sha256Hex(canonicalJson(
|
|
1136
|
+
assessmentDigest: sha256Hex(canonicalJson(digestPayload)),
|
|
652
1137
|
topology,
|
|
653
1138
|
findings: sortedFindings,
|
|
654
1139
|
hookDurations,
|
|
@@ -656,8 +1141,18 @@ export function buildAssessmentReport({
|
|
|
656
1141
|
gateDiagnostics,
|
|
657
1142
|
workflowPrerequisites,
|
|
658
1143
|
unobserved,
|
|
1144
|
+
hostCoverage: publicCoverage,
|
|
659
1145
|
next,
|
|
660
|
-
summary: renderSummary({
|
|
1146
|
+
summary: renderSummary({
|
|
1147
|
+
config,
|
|
1148
|
+
status,
|
|
1149
|
+
findings: sortedFindings,
|
|
1150
|
+
topology,
|
|
1151
|
+
hookDurations,
|
|
1152
|
+
gateSuggestions,
|
|
1153
|
+
next,
|
|
1154
|
+
hostCoverage: publicCoverage,
|
|
1155
|
+
}),
|
|
661
1156
|
};
|
|
662
1157
|
return Object.freeze(report);
|
|
663
1158
|
}
|
|
@@ -668,13 +1163,16 @@ export function buildAssessmentReport({
|
|
|
668
1163
|
* @param {Object} input
|
|
669
1164
|
* @returns {string}
|
|
670
1165
|
*/
|
|
671
|
-
function renderSummary({ config, status, findings, topology, gateSuggestions, next }) {
|
|
1166
|
+
function renderSummary({ config, status, findings, topology, gateSuggestions, next, hostCoverage }) {
|
|
672
1167
|
const lines = [];
|
|
673
1168
|
lines.push(`项目: ${config.project?.name ?? '(未命名)'}`);
|
|
674
1169
|
lines.push(`拓扑: ${topology.type},发布单元 ${topology.releaseUnits.length} 个${topology.distributions.length > 0 ? `,分发渠道 ${topology.distributions.join(', ')}` : ''}`);
|
|
675
1170
|
lines.push(`接入状态: ${STATUS_LABEL[status] ?? status}`);
|
|
676
1171
|
const count = (category) => findings.filter((f) => f.category === category).length;
|
|
677
1172
|
lines.push(`必选缺口 ${count('mandatory-gap')} | 已满足 ${count('satisfied')} | 可选建议 ${count('optional-suggestion')} | 不适用 ${count('not-applicable')}`);
|
|
1173
|
+
if (hostCoverage?.requested) {
|
|
1174
|
+
lines.push(`宿主覆盖: 已请求,${hostCoverage.complete ? '事实完整' : '事实不完整'},${hostCoverage.results.length} 项结果`);
|
|
1175
|
+
}
|
|
678
1176
|
const blocking = findings.filter((f) => f.category === FINDING_CATEGORY.MANDATORY_GAP);
|
|
679
1177
|
if (blocking.length > 0) {
|
|
680
1178
|
lines.push('阻断项:');
|