canary-test-cli 6.8.0 → 7.0.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/dist/engine/analysis/cli.js +155 -45
- package/dist/engine/analysis/engine.js +9 -9
- package/dist/engine/analysis/reports.js +0 -0
- package/dist/engine/cli-commands.js +2 -2
- package/dist/engine/core/migrator.js +142 -31
- package/dist/engine/core/static-linter.js +2 -2
- package/dist/engine/core/workspace-detect.js +0 -0
- package/dist/engine/guardian/adjudication.js +1 -1
- package/dist/engine/guardian/agent-tier.js +3 -3
- package/dist/engine/guardian/coverage.js +15 -1397
- package/dist/engine/guardian/diff-coverage/formats/cobertura.js +130 -0
- package/dist/engine/guardian/diff-coverage/formats/coverage-json-lint.js +197 -0
- package/dist/engine/guardian/diff-coverage/formats/coverage-json.js +107 -0
- package/dist/engine/guardian/diff-coverage/formats/xml.js +151 -0
- package/dist/engine/guardian/diff-coverage/graph-tier.js +223 -0
- package/dist/engine/guardian/diff-coverage/heuristic-tier.js +150 -0
- package/dist/engine/guardian/diff-coverage/orchestrator.js +125 -0
- package/dist/engine/guardian/diff-coverage/paths.js +164 -0
- package/dist/engine/guardian/diff-coverage/report-tier.js +153 -0
- package/dist/engine/guardian/diff-coverage/type-only.js +150 -0
- package/dist/engine/guardian/diff-coverage/types.js +115 -0
- package/dist/engine/guardian/pr-check.js +22 -7
- package/dist/engine-checks.d.ts +15 -0
- package/dist/engine-checks.js +92 -1
- package/dist/overlay-commands.d.ts +12 -1
- package/dist/overlay-commands.js +28 -2
- package/dist/router.js +17 -5
- package/dist/uninstall-render.d.ts +11 -0
- package/dist/uninstall-render.js +60 -0
- package/dist/uninstall-scan.d.ts +14 -0
- package/dist/uninstall-scan.js +273 -0
- package/dist/uninstall-types.d.ts +46 -0
- package/dist/uninstall-types.js +91 -0
- package/dist/uninstall.d.ts +13 -0
- package/dist/uninstall.js +174 -0
- package/package.json +1 -1
|
@@ -248,6 +248,23 @@ function resolveTemplatePath(skillDir, rel) {
|
|
|
248
248
|
return null;
|
|
249
249
|
return full;
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Is the deployed copy of a skill the consumer's rather than the overlay's?
|
|
253
|
+
*
|
|
254
|
+
* True when it differs from the overlay AND from the hash recorded at deploy
|
|
255
|
+
* time -- or carries no recorded provenance at all, since unknown provenance is
|
|
256
|
+
* not permission to overwrite.
|
|
257
|
+
*
|
|
258
|
+
* This is the single definition of `local_edit`. The skill-deploy phase refuses
|
|
259
|
+
* to overwrite on it (#334) and the workflow-install phase withholds on it
|
|
260
|
+
* (#667); sharing one predicate is what keeps the two phases from reporting
|
|
261
|
+
* contradictory things about the same skill in the same run.
|
|
262
|
+
*/
|
|
263
|
+
function isLocallyEdited(overlayHash, targetHash, recordedHash) {
|
|
264
|
+
if (targetHash === overlayHash)
|
|
265
|
+
return false;
|
|
266
|
+
return recordedHash === undefined || targetHash !== recordedHash;
|
|
267
|
+
}
|
|
251
268
|
// ---------------------------------------------------------------------------
|
|
252
269
|
// Constants / probes
|
|
253
270
|
// ---------------------------------------------------------------------------
|
|
@@ -455,8 +472,10 @@ export class SkillDeployResult {
|
|
|
455
472
|
* - `dry_run` -- what an `--apply` run would have done
|
|
456
473
|
* - `missing` -- the overlay declares a template it does not ship
|
|
457
474
|
* - `invalid` -- the declared path escapes the skill directory (refused)
|
|
475
|
+
* - `withheld` -- the declaring skill was skipped as locally edited, so its
|
|
476
|
+
* templates were not installed either (#667)
|
|
458
477
|
*
|
|
459
|
-
* `outdated` and `
|
|
478
|
+
* `outdated`, `conflict`, and `withheld` are REPORTS. None ever writes.
|
|
460
479
|
*/
|
|
461
480
|
export class WorkflowInstallResult {
|
|
462
481
|
/** File name under `.github/workflows/`. */
|
|
@@ -506,11 +525,21 @@ export class FreshnessReport {
|
|
|
506
525
|
* nagging about something canary has no claim over.
|
|
507
526
|
*/
|
|
508
527
|
workflows;
|
|
509
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Every shape the gate actually resolved -- the set `migrate` deploys for
|
|
530
|
+
* (#504 part 1). The scalar `shape` above stays, because the `--check --json`
|
|
531
|
+
* payload is documented and consumers read it; `shapes` is additive.
|
|
532
|
+
*
|
|
533
|
+
* A mixed monorepo collapses `shape` to `unknown` while `shapes` still names
|
|
534
|
+
* both, so the two disagree by design rather than by accident.
|
|
535
|
+
*/
|
|
536
|
+
shapes;
|
|
537
|
+
constructor(shape, overlay_path = null, results = [], workflows = [], shapes = []) {
|
|
510
538
|
this.shape = shape;
|
|
511
539
|
this.overlay_path = overlay_path;
|
|
512
540
|
this.results = results;
|
|
513
541
|
this.workflows = workflows;
|
|
542
|
+
this.shapes = shapes;
|
|
514
543
|
}
|
|
515
544
|
get stale() {
|
|
516
545
|
return this.results.filter((r) => r.status === 'stale' || r.status === 'missing');
|
|
@@ -564,6 +593,7 @@ export class FreshnessReport {
|
|
|
564
593
|
to_dict() {
|
|
565
594
|
return {
|
|
566
595
|
shape: this.shape,
|
|
596
|
+
shapes: this.shapes,
|
|
567
597
|
overlay_path: this.overlay_path,
|
|
568
598
|
in_sync: this.in_sync,
|
|
569
599
|
has_drift: this.has_drift,
|
|
@@ -587,14 +617,20 @@ export class FreshnessReport {
|
|
|
587
617
|
`**Shape:** ${this.shape}`,
|
|
588
618
|
'',
|
|
589
619
|
];
|
|
620
|
+
// A workspace repo resolves several shapes at once; naming only the scalar
|
|
621
|
+
// would tell a reader `unknown` while the gate checked two shapes (#504).
|
|
622
|
+
if (this.shapes.length > 1) {
|
|
623
|
+
lines.push(`**Shapes checked:** ${this.shapes.map((s) => `\`${s}\``).join(', ')}`, '');
|
|
624
|
+
}
|
|
590
625
|
if (this.results.length === 0) {
|
|
591
626
|
lines.push("_No overlay skills match this project's shape._", '');
|
|
592
627
|
lines.push(`${WARN} **Abstained** ${EMDASH} the gate verified zero skills, so this is not a pass.`, '');
|
|
593
|
-
if (this.
|
|
628
|
+
if (this.shapes.length === 0) {
|
|
594
629
|
lines.push('The shape could not be detected. Set `canary_shape` in', '`.canary/company.json` or pass `--framework <name>`.', '');
|
|
595
630
|
}
|
|
596
631
|
else {
|
|
597
|
-
|
|
632
|
+
const covered = this.shapes.map((s) => `\`${s}\``).join(', ');
|
|
633
|
+
lines.push(`The overlay ships no skills with \`deploy_to\` covering ${covered}.`, "Check the overlay's `deploy_to` lists or the resolved `canary_shape`.", '');
|
|
598
634
|
}
|
|
599
635
|
lines.push(...workflowMarkdown(this.workflows, false));
|
|
600
636
|
return lines.join('\n');
|
|
@@ -654,6 +690,14 @@ function workflowMarkdown(results, dryRun) {
|
|
|
654
690
|
'differs from the template. Re-run with `--force` to take the ' +
|
|
655
691
|
"overlay's version.", '');
|
|
656
692
|
}
|
|
693
|
+
// #667: a withheld install is the one line that says why a template the
|
|
694
|
+
// overlay declares is absent from the target -- without it, the section
|
|
695
|
+
// simply omits the file and reads as though it were never declared.
|
|
696
|
+
if (results.some((r) => r.status === 'withheld')) {
|
|
697
|
+
lines.push(`${WARN} Templates declared by a skill with local edits were withheld: ` +
|
|
698
|
+
'the skill itself was skipped, so its workflows were not installed ' +
|
|
699
|
+
'either. Revert the local edits, or re-run with `--force`.', '');
|
|
700
|
+
}
|
|
657
701
|
return lines;
|
|
658
702
|
}
|
|
659
703
|
export class MigrationReport {
|
|
@@ -994,6 +1038,10 @@ export class HarnessMigrator {
|
|
|
994
1038
|
? shapeForFrameworkOverride(framework, projectRoot)
|
|
995
1039
|
: null;
|
|
996
1040
|
const shape = overrideShape ?? ctx.detected_shape;
|
|
1041
|
+
// The set that actually drives deployment. Re-derived from the *effective*
|
|
1042
|
+
// scalar rather than reusing `ctx.shapes`, so a `--framework` override
|
|
1043
|
+
// contributes its resolved shape to the union too (#504 parts 1-2).
|
|
1044
|
+
const deployShapes = unionShapes(shape, ctx.workspace);
|
|
997
1045
|
const followups = [];
|
|
998
1046
|
if (effectiveFramework === null) {
|
|
999
1047
|
followups.push(uncertainDetectionMessage('test framework', {
|
|
@@ -1004,7 +1052,7 @@ export class HarnessMigrator {
|
|
|
1004
1052
|
// Issue #295 point 3: a detection miss must not block skill deployment --
|
|
1005
1053
|
// nor, for the same reason, the workflow install (#459). The guardian
|
|
1006
1054
|
// workflow is exactly what an unrecognised repo most needs.
|
|
1007
|
-
const deployed = this.deploySkills(
|
|
1055
|
+
const deployed = this.deploySkills(deployShapes, overlayPath, projectRoot, dryRun);
|
|
1008
1056
|
return new MigrationReport({
|
|
1009
1057
|
framework: 'unknown',
|
|
1010
1058
|
shape,
|
|
@@ -1014,9 +1062,9 @@ export class HarnessMigrator {
|
|
|
1014
1062
|
manual_followups: followups,
|
|
1015
1063
|
config_warnings: ctx.config_warnings,
|
|
1016
1064
|
workspace: ctx.workspace,
|
|
1017
|
-
shapes:
|
|
1065
|
+
shapes: deployShapes,
|
|
1018
1066
|
deployed_skills: deployed,
|
|
1019
|
-
installed_workflows: this.installWorkflows(
|
|
1067
|
+
installed_workflows: this.installWorkflows(deployShapes, overlayPath, projectRoot, dryRun, force),
|
|
1020
1068
|
});
|
|
1021
1069
|
}
|
|
1022
1070
|
// A framework canary knows but cannot scaffold gets no config boilerplate.
|
|
@@ -1035,10 +1083,10 @@ export class HarnessMigrator {
|
|
|
1035
1083
|
// must not be offered a second one at the root.
|
|
1036
1084
|
const existingSuites = findWorkspaceSuites(projectRoot, effectiveFramework);
|
|
1037
1085
|
const scaffolder = new Scaffolder();
|
|
1038
|
-
const deployed = this.deploySkills(
|
|
1086
|
+
const deployed = this.deploySkills(deployShapes, overlayPath, projectRoot, dryRun);
|
|
1039
1087
|
// Post-copy install phase: the template bytes already landed under
|
|
1040
1088
|
// .canary/skills/ with the skill; this puts them where Actions looks.
|
|
1041
|
-
const installedWorkflows = this.installWorkflows(
|
|
1089
|
+
const installedWorkflows = this.installWorkflows(deployShapes, overlayPath, projectRoot, dryRun, force);
|
|
1042
1090
|
if (dryRun) {
|
|
1043
1091
|
const tmpl = TEMPLATES[effectiveFramework];
|
|
1044
1092
|
const files = tmpl?.files ?? {};
|
|
@@ -1067,7 +1115,7 @@ export class HarnessMigrator {
|
|
|
1067
1115
|
installed_workflows: installedWorkflows,
|
|
1068
1116
|
config_warnings: ctx.config_warnings,
|
|
1069
1117
|
workspace: ctx.workspace,
|
|
1070
|
-
shapes:
|
|
1118
|
+
shapes: deployShapes,
|
|
1071
1119
|
});
|
|
1072
1120
|
}
|
|
1073
1121
|
// Same guard on the apply path: what the dry run refuses to propose, an
|
|
@@ -1092,16 +1140,25 @@ export class HarnessMigrator {
|
|
|
1092
1140
|
installed_workflows: installedWorkflows,
|
|
1093
1141
|
config_warnings: ctx.config_warnings,
|
|
1094
1142
|
workspace: ctx.workspace,
|
|
1095
|
-
shapes:
|
|
1143
|
+
shapes: deployShapes,
|
|
1096
1144
|
});
|
|
1097
1145
|
}
|
|
1098
1146
|
// -- private helpers --------------------------------------------------------
|
|
1099
1147
|
/**
|
|
1100
1148
|
* Return `[[SkillInfo, skillDir], ...]` for overlay skills whose `deploy_to`
|
|
1101
|
-
* matches
|
|
1102
|
-
* `~/.canary/skills/`. The first definition of a
|
|
1149
|
+
* matches **any** shape in *shapes* (or the `all` sentinel). Sources:
|
|
1150
|
+
* *overlayPath* first, then `~/.canary/skills/`. The first definition of a
|
|
1151
|
+
* name wins.
|
|
1152
|
+
*
|
|
1153
|
+
* *shapes* is a set, not a scalar, because a monorepo genuinely has several
|
|
1154
|
+
* shapes at once (#504 part 1). One pass over the overlay collects the union
|
|
1155
|
+
* deduplicated by name, so a skill matching two shapes is collected once and
|
|
1156
|
+
* the deploy manifest is read and written exactly once -- which a per-shape
|
|
1157
|
+
* loop would not give.
|
|
1158
|
+
*
|
|
1159
|
+
* An empty *shapes* is the old `unknown`: only `deploy_to: [all]` matches.
|
|
1103
1160
|
*/
|
|
1104
|
-
collectOverlaySkills(
|
|
1161
|
+
collectOverlaySkills(shapes, overlayPath) {
|
|
1105
1162
|
const candidateRoots = [];
|
|
1106
1163
|
if (overlayPath !== null)
|
|
1107
1164
|
candidateRoots.push(overlayPath);
|
|
@@ -1143,7 +1200,7 @@ export class HarnessMigrator {
|
|
|
1143
1200
|
continue;
|
|
1144
1201
|
if (!pyTruthy(info.deploy_to))
|
|
1145
1202
|
continue;
|
|
1146
|
-
if (!info.deploy_to.includes(
|
|
1203
|
+
if (!shapes.some((s) => info.deploy_to.includes(s)) &&
|
|
1147
1204
|
!info.deploy_to.includes('all')) {
|
|
1148
1205
|
continue;
|
|
1149
1206
|
}
|
|
@@ -1154,12 +1211,12 @@ export class HarnessMigrator {
|
|
|
1154
1211
|
return collected;
|
|
1155
1212
|
}
|
|
1156
1213
|
/**
|
|
1157
|
-
* Copy skills from the overlay's `.canary/skills/`
|
|
1214
|
+
* Copy skills from the overlay's `.canary/skills/` matching any of *shapes*.
|
|
1158
1215
|
* Deployment is strictly one-way -- the overlay owns deployed files (#334).
|
|
1159
1216
|
*/
|
|
1160
|
-
deploySkills(
|
|
1217
|
+
deploySkills(shapes, overlayPath, targetRoot, dryRun) {
|
|
1161
1218
|
const results = [];
|
|
1162
|
-
const skillsToDeploy = this.collectOverlaySkills(
|
|
1219
|
+
const skillsToDeploy = this.collectOverlaySkills(shapes, overlayPath);
|
|
1163
1220
|
const targetSkillsDir = join(targetRoot, '.canary', 'skills');
|
|
1164
1221
|
const doc = readManifestDoc(targetSkillsDir);
|
|
1165
1222
|
const manifest = doc.skills;
|
|
@@ -1178,8 +1235,7 @@ export class HarnessMigrator {
|
|
|
1178
1235
|
}
|
|
1179
1236
|
continue;
|
|
1180
1237
|
}
|
|
1181
|
-
|
|
1182
|
-
if (recorded === undefined || targetHash !== recorded) {
|
|
1238
|
+
if (isLocallyEdited(overlayHash, targetHash, manifest[dirName]?.hash)) {
|
|
1183
1239
|
// Hand-edited (or unprovenanced) -- one-way ownership refuses to clobber.
|
|
1184
1240
|
results.push(new SkillDeployResult(info.name, 'skipped', `local edits ${EMDASH} not overwritten`));
|
|
1185
1241
|
continue;
|
|
@@ -1210,6 +1266,21 @@ export class HarnessMigrator {
|
|
|
1210
1266
|
writeManifestDoc(targetSkillsDir, doc);
|
|
1211
1267
|
return results;
|
|
1212
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Would the deploy phase skip this skill as `local_edit` (#667)?
|
|
1271
|
+
*
|
|
1272
|
+
* Answers for the CURRENT on-disk state, so it holds whether the deploy phase
|
|
1273
|
+
* has already run this invocation (apply) or never will (dry run / `--check`)
|
|
1274
|
+
* -- the workflow report then says the same thing about a skill as the skill
|
|
1275
|
+
* report does.
|
|
1276
|
+
*/
|
|
1277
|
+
skillWasSkipped(skillDir, targetSkillsDir, manifest) {
|
|
1278
|
+
const dirName = basename(skillDir);
|
|
1279
|
+
const dest = join(targetSkillsDir, dirName);
|
|
1280
|
+
if (!existsSync(dest))
|
|
1281
|
+
return false;
|
|
1282
|
+
return isLocallyEdited(hashSkillDir(skillDir), hashSkillDir(dest), manifest[dirName]?.hash);
|
|
1283
|
+
}
|
|
1213
1284
|
/**
|
|
1214
1285
|
* Install the workflow templates the shape-matching overlay skills declare
|
|
1215
1286
|
* into the target's `.github/workflows/` (#459).
|
|
@@ -1225,22 +1296,43 @@ export class HarnessMigrator {
|
|
|
1225
1296
|
* a hand-tuned workflow -- or nagging that it is "stale" via an exit code --
|
|
1226
1297
|
* would be a worse failure than the partial adoption this fixes.
|
|
1227
1298
|
*
|
|
1228
|
-
*
|
|
1229
|
-
* `
|
|
1230
|
-
*
|
|
1299
|
+
* **The skill's own protection extends here (#667).** A skill the deploy
|
|
1300
|
+
* phase skips as `local_edit` installs nothing: it is reported `withheld`
|
|
1301
|
+
* with the reason and no bytes are written. Otherwise a run could announce
|
|
1302
|
+
* "skipped -- local edits, not overwritten" while writing that same skill's
|
|
1303
|
+
* files to `.github/workflows/` anyway, which is how a consumer who
|
|
1304
|
+
* deliberately de-listed a template got it reinstated on every sync. The
|
|
1305
|
+
* de-listing lives in the consumer's SKILL.md, and the install list is read
|
|
1306
|
+
* from the OVERLAY copy -- so withholding is also the only way that edit can
|
|
1307
|
+
* mean anything.
|
|
1308
|
+
*
|
|
1309
|
+
* Shape selection reuses the same resolved shape set that drives `deploy_to`
|
|
1310
|
+
* matching: skills are gated by {@link collectOverlaySkills}, and an entry may
|
|
1311
|
+
* additionally carry a `<shape>:` prefix to pick a variant. A prefixed entry
|
|
1312
|
+
* installs when its shape is anywhere in *shapes* (#504 part 1) -- a monorepo
|
|
1313
|
+
* carrying both an e2e and a unit package wants both variants, and matching
|
|
1314
|
+
* only the scalar would silently install neither once the scalar collapses to
|
|
1315
|
+
* `unknown`.
|
|
1231
1316
|
*/
|
|
1232
|
-
installWorkflows(
|
|
1317
|
+
installWorkflows(shapes, overlayPath, targetRoot, dryRun, force = false) {
|
|
1233
1318
|
const results = [];
|
|
1234
|
-
const skills = this.collectOverlaySkills(
|
|
1319
|
+
const skills = this.collectOverlaySkills(shapes, overlayPath);
|
|
1235
1320
|
const targetSkillsDir = join(targetRoot, '.canary', 'skills');
|
|
1236
1321
|
const doc = readManifestDoc(targetSkillsDir);
|
|
1237
1322
|
const workflowsDir = join(targetRoot, '.github', 'workflows');
|
|
1238
1323
|
let manifestDirty = false;
|
|
1239
1324
|
for (const [info, skillDir] of skills) {
|
|
1240
1325
|
const { entries, version } = readWorkflowDeclaration(info.path);
|
|
1326
|
+
// Hashing a skill directory is not free, so only skills that actually
|
|
1327
|
+
// declare templates pay for the local-edit lookup.
|
|
1328
|
+
const skillWithheld = !force &&
|
|
1329
|
+
entries.length > 0 &&
|
|
1330
|
+
this.skillWasSkipped(skillDir, targetSkillsDir, doc.skills);
|
|
1241
1331
|
for (const entry of entries) {
|
|
1242
1332
|
const [wantShape, rel] = parseWorkflowEntry(entry);
|
|
1243
|
-
if (wantShape !== null &&
|
|
1333
|
+
if (wantShape !== null &&
|
|
1334
|
+
!shapes.includes(wantShape) &&
|
|
1335
|
+
wantShape !== 'all') {
|
|
1244
1336
|
continue;
|
|
1245
1337
|
}
|
|
1246
1338
|
const src = resolveTemplatePath(skillDir, rel);
|
|
@@ -1274,6 +1366,21 @@ export class HarnessMigrator {
|
|
|
1274
1366
|
const push = (status, detail) => {
|
|
1275
1367
|
results.push(new WorkflowInstallResult(name, info.name, status, detail));
|
|
1276
1368
|
};
|
|
1369
|
+
const installedBytes = existsSync(dest) ? readBytesOrNull(dest) : null;
|
|
1370
|
+
const alreadyCurrent = installedBytes !== null && installedBytes.equals(templateBytes);
|
|
1371
|
+
// #667: the declaring skill was skipped as locally edited, so this
|
|
1372
|
+
// template is not ours to place. An already-identical file is exempt:
|
|
1373
|
+
// it is a no-op, and calling that "withheld" would raise a finding on
|
|
1374
|
+
// every scheduled freshness check. Everything else is gated here,
|
|
1375
|
+
// ahead of `outdated`, whose remedy ("re-run with --force") would
|
|
1376
|
+
// otherwise talk a consumer into the very overwrite the skip refused.
|
|
1377
|
+
if (skillWithheld && !alreadyCurrent) {
|
|
1378
|
+
push('withheld', `.github/workflows/${name} was NOT installed ${EMDASH} its skill ` +
|
|
1379
|
+
`'${info.name}' has local edits and was skipped, so its ` +
|
|
1380
|
+
'templates were withheld too; re-run with --force to install it ' +
|
|
1381
|
+
'anyway');
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1277
1384
|
if (!existsSync(dest)) {
|
|
1278
1385
|
if (dryRun) {
|
|
1279
1386
|
push('dry_run', `would install .github/workflows/${name} (v${version})`);
|
|
@@ -1283,8 +1390,7 @@ export class HarnessMigrator {
|
|
|
1283
1390
|
push('installed', `installed .github/workflows/${name} (v${version})`);
|
|
1284
1391
|
continue;
|
|
1285
1392
|
}
|
|
1286
|
-
|
|
1287
|
-
if (installedBytes !== null && installedBytes.equals(templateBytes)) {
|
|
1393
|
+
if (alreadyCurrent) {
|
|
1288
1394
|
// Back-fill provenance for a hand-placed but identical file so a later
|
|
1289
1395
|
// template fix can be reported as `outdated` rather than `conflict`.
|
|
1290
1396
|
if (doc.workflows[name]?.hash !== templateHash)
|
|
@@ -1334,7 +1440,12 @@ export class HarnessMigrator {
|
|
|
1334
1440
|
'Expected harness.config.json and .harness/ directory.');
|
|
1335
1441
|
}
|
|
1336
1442
|
const shape = ctx.detected_shape;
|
|
1337
|
-
|
|
1443
|
+
// The gate must examine exactly what `migrate` deploys. Resolving only the
|
|
1444
|
+
// scalar here would let `--check` report "in sync" about skills it never
|
|
1445
|
+
// looked at in a monorepo -- a deliberate false green in a drift gate
|
|
1446
|
+
// (#504 part 1).
|
|
1447
|
+
const shapes = ctx.shapes;
|
|
1448
|
+
const skills = this.collectOverlaySkills(shapes, overlayPath);
|
|
1338
1449
|
const targetSkillsDir = join(projectRoot, '.canary', 'skills');
|
|
1339
1450
|
const manifest = readManifestDoc(targetSkillsDir).skills;
|
|
1340
1451
|
const results = [];
|
|
@@ -1362,7 +1473,7 @@ export class HarnessMigrator {
|
|
|
1362
1473
|
return new FreshnessReport(shape, overlayPath !== null ? String(overlayPath) : null, results,
|
|
1363
1474
|
// dryRun = true: `--check` reports what an install WOULD do and never
|
|
1364
1475
|
// writes. Informational only -- see FreshnessReport.workflows.
|
|
1365
|
-
this.installWorkflows(
|
|
1476
|
+
this.installWorkflows(shapes, overlayPath, projectRoot, true, false), shapes);
|
|
1366
1477
|
}
|
|
1367
1478
|
detectFramework(root, config, ws = null) {
|
|
1368
1479
|
// Explicit override in .canary/company.json ("canary_shape" field) is
|
|
@@ -1400,7 +1511,7 @@ export class HarnessMigrator {
|
|
|
1400
1511
|
const findings = ws.findings;
|
|
1401
1512
|
if (findings.length === 0)
|
|
1402
1513
|
return null;
|
|
1403
|
-
const pairs = new Set(findings.map((f) => `${f.framework}
|
|
1514
|
+
const pairs = new Set(findings.map((f) => `${f.framework}\0${f.shape}`));
|
|
1404
1515
|
if (pairs.size > 1) {
|
|
1405
1516
|
return [null, 'unknown', 'workspace (mixed)', 'none'];
|
|
1406
1517
|
}
|
|
@@ -347,7 +347,7 @@ const JS_EXT_SET = new Set(JS_TEST_EXTENSIONS);
|
|
|
347
347
|
* guess that cannot be distinguished from a clean result is a false green;
|
|
348
348
|
* `null` forces the caller to abstain instead.
|
|
349
349
|
*/
|
|
350
|
-
export function
|
|
350
|
+
export function frameworkForPath(path) {
|
|
351
351
|
const suffix = extname(path).toLowerCase();
|
|
352
352
|
const name = basename(path).toLowerCase();
|
|
353
353
|
if (suffix === '.py')
|
|
@@ -368,7 +368,7 @@ export class UnsupportedTestFileError extends Error {
|
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
370
|
function requireFramework(path, framework) {
|
|
371
|
-
const fw = framework ||
|
|
371
|
+
const fw = framework || frameworkForPath(path);
|
|
372
372
|
if (fw === null)
|
|
373
373
|
throw new UnsupportedTestFileError(path);
|
|
374
374
|
return fw;
|
|
Binary file
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `tier.ts`), which imports it never (SC-11). Under the chosen **Option A**
|
|
9
9
|
* invocation mechanism it **never calls an LLM directly**: it (i) turns
|
|
10
10
|
* deterministic Tier-0 gaps into structured authoring intents, (ii) parses and
|
|
11
|
-
* validates agent results into `
|
|
11
|
+
* validates agent results into `GuardianFinding`/`GeneratedTest` records, and (iii)
|
|
12
12
|
* enforces the write-safety model (opt-in, fork, collision, loop-guard) and the
|
|
13
13
|
* block-once decision. It reaches an agent runtime only through an injected
|
|
14
14
|
* `AgentInvoker` **port**; the production default (`RecordingInvoker`) records
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { existsSync } from 'node:fs';
|
|
28
28
|
import { Severity } from './impact-mapper.js';
|
|
29
|
-
import {
|
|
29
|
+
import { GuardianFinding } from './pr-check.js';
|
|
30
30
|
/** Construct a {@link ReviewRequest} (a frozen dataclass in Python). */
|
|
31
31
|
function reviewRequest(test_paths) {
|
|
32
32
|
return { test_paths };
|
|
@@ -98,7 +98,7 @@ function parseReviewFindings(transcript) {
|
|
|
98
98
|
if (severity === undefined)
|
|
99
99
|
continue;
|
|
100
100
|
const path = match.groups['path'];
|
|
101
|
-
findings.push(new
|
|
101
|
+
findings.push(new GuardianFinding({
|
|
102
102
|
path,
|
|
103
103
|
unit: path,
|
|
104
104
|
kind: 'weak-test',
|