@codyswann/lisa 3.4.0 → 3.5.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/README.md +22 -0
- package/all/copy-contents/gitignore +7 -0
- package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +44 -4
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +31 -0
- package/all/copy-overwrite/scripts/lisa-schema-validate.mjs +125 -47
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +24 -2
- package/cdk/merge/.oxlintrc.json +1 -1
- package/dist/cli/apply.d.ts.map +1 -1
- package/dist/cli/apply.js +48 -13
- package/dist/cli/apply.js.map +1 -1
- package/dist/cli/doctor-apply-freshness.d.ts +29 -0
- package/dist/cli/doctor-apply-freshness.d.ts.map +1 -0
- package/dist/cli/doctor-apply-freshness.js +194 -0
- package/dist/cli/doctor-apply-freshness.js.map +1 -0
- package/dist/cli/doctor-learnings-merge-driver.d.ts +23 -0
- package/dist/cli/doctor-learnings-merge-driver.d.ts.map +1 -0
- package/dist/cli/doctor-learnings-merge-driver.js +73 -0
- package/dist/cli/doctor-learnings-merge-driver.js.map +1 -0
- package/dist/cli/doctor-readiness-workflows.js +2 -2
- package/dist/cli/doctor-readiness-workflows.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/ui-ci-quality-jobs-parse.js +2 -2
- package/dist/cli/ui-ci-quality-jobs-parse.js.map +1 -1
- package/dist/cli/ui-deploy-pipeline-model.d.ts +4 -0
- package/dist/cli/ui-deploy-pipeline-model.d.ts.map +1 -1
- package/dist/cli/ui-deploy-pipeline-model.js +2 -2
- package/dist/cli/ui-deploy-pipeline-model.js.map +1 -1
- package/dist/codex/agent-transformer.js +2 -2
- package/dist/codex/agent-transformer.js.map +1 -1
- package/dist/codex/command-skill-transformer.js +3 -3
- package/dist/codex/command-skill-transformer.js.map +1 -1
- package/dist/codex/scripts/block-no-verify.sh +53 -4
- package/dist/core/apply-receipt.d.ts +68 -0
- package/dist/core/apply-receipt.d.ts.map +1 -0
- package/dist/core/apply-receipt.js +107 -0
- package/dist/core/apply-receipt.js.map +1 -0
- package/dist/core/learnings-merge-driver-install.d.ts +34 -0
- package/dist/core/learnings-merge-driver-install.d.ts.map +1 -1
- package/dist/core/learnings-merge-driver-install.js +36 -0
- package/dist/core/learnings-merge-driver-install.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +62 -39
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/migrations/ensure-lisa-postinstall.d.ts +33 -0
- package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
- package/dist/migrations/ensure-lisa-postinstall.js +47 -5
- package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
- package/dist/migrations/ensure-oxlint-base-configs.d.ts +42 -0
- package/dist/migrations/ensure-oxlint-base-configs.d.ts.map +1 -0
- package/dist/migrations/ensure-oxlint-base-configs.js +212 -0
- package/dist/migrations/ensure-oxlint-base-configs.js.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/opencode/command-transformer.js +3 -3
- package/dist/opencode/command-transformer.js.map +1 -1
- package/dist/utils/yaml.d.ts +54 -0
- package/dist/utils/yaml.d.ts.map +1 -0
- package/dist/utils/yaml.js +124 -0
- package/dist/utils/yaml.js.map +1 -0
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +104 -19
- package/expo/copy-overwrite/scripts/bdd/contract.mjs +20 -1
- package/expo/copy-overwrite/scripts/bdd/discover.mjs +53 -10
- package/expo/copy-overwrite/scripts/bdd/parse.mjs +25 -3
- package/expo/copy-overwrite/scripts/bdd/render.mjs +2 -1
- package/expo/copy-overwrite/scripts/bdd/report.mjs +26 -12
- package/expo/copy-overwrite/scripts/bdd/validate.mjs +2 -2
- package/expo/copy-overwrite/scripts/bdd/waivers.mjs +19 -4
- package/expo/copy-overwrite/scripts/bdd-matrix.mjs +2 -2
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +58 -16
- package/expo/create-only/.github/required-checks.json +10 -4
- package/expo/merge/.oxlintrc.json +1 -1
- package/harper-fabric/merge/.oxlintrc.json +1 -1
- package/nestjs/create-only/.github/required-checks.json +57 -0
- package/nestjs/merge/.oxlintrc.json +1 -1
- package/package.json +1 -1
- package/phaser/merge/.oxlintrc.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa/hooks/block-no-verify.agy.sh +53 -4
- package/plugins/lisa/hooks/block-no-verify.sh +31 -0
- package/plugins/lisa/rules/eager/credential-substrate-precedence.md +3 -1
- package/plugins/lisa/rules/eager/tracked-work.md +1 -1
- package/plugins/lisa/rules/reference/credential-substrate-precedence.md +20 -0
- package/plugins/lisa/rules/reference/tracked-work.md +4 -2
- package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +53 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa-agy/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +31 -0
- package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +3 -1
- package/plugins/lisa-copilot/rules/eager/tracked-work.md +1 -1
- package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +20 -0
- package/plugins/lisa-copilot/rules/reference/tracked-work.md +4 -2
- package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa-copilot/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +31 -0
- package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +20 -0
- package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +3 -1
- package/plugins/lisa-cursor/rules/tracked-work-reference.mdc +4 -2
- package/plugins/lisa-cursor/rules/tracked-work.mdc +1 -1
- package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/lisa-cursor/skills/lisa-track/SKILL.md +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-no-verify.agy.sh +53 -4
- package/plugins/src/base/hooks/block-no-verify.sh +31 -0
- package/plugins/src/base/rules/eager/credential-substrate-precedence.md +3 -1
- package/plugins/src/base/rules/eager/tracked-work.md +1 -1
- package/plugins/src/base/rules/reference/credential-substrate-precedence.md +20 -0
- package/plugins/src/base/rules/reference/tracked-work.md +4 -2
- package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +7 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-jam-access/SKILL.md +37 -2
- package/plugins/src/base/skills/lisa-notion-access/SKILL.md +7 -0
- package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +40 -3
- package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +44 -3
- package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +56 -0
- package/plugins/src/base/skills/lisa-track/SKILL.md +1 -1
- package/scripts/install-claude-plugins.sh +5 -1
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +298 -26
- package/typescript/create-only/.github/required-checks.json +11 -6
- package/typescript/create-only/.github/workflows/required-checks-drift.yml +84 -0
- package/typescript/merge/.oxlintrc.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single js-yaml entry point for all of Lisa.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists rather than `import yaml from "js-yaml"` at each call site:
|
|
5
|
+
* Lisa is `"type": "module"`, so a default import compiles to a real ESM
|
|
6
|
+
* default import. js-yaml 5.x ships an ESM build that exports `load`/`dump`
|
|
7
|
+
* as *named* exports and no `default`, so the default import is a link-time
|
|
8
|
+
* `SyntaxError: The requested module 'js-yaml' does not provide an export
|
|
9
|
+
* named 'default'`. That error fires while the module graph is being
|
|
10
|
+
* instantiated — before any Lisa code runs — so the whole CLI is dead,
|
|
11
|
+
* including `lisa doctor`. Lisa declares js-yaml `^4.3.1` for itself, but a
|
|
12
|
+
* host project's `overrides`/`resolutions` collapse Lisa's nested copy onto
|
|
13
|
+
* whatever the host pinned, which is exactly how geminisportsai/frontend-v2
|
|
14
|
+
* ended up running Lisa against js-yaml 5 (CodySwannGT/lisa#2467).
|
|
15
|
+
*
|
|
16
|
+
* A namespace import links against any module shape, so the incompatibility
|
|
17
|
+
* becomes a *runtime* condition this module can inspect and report in plain
|
|
18
|
+
* language instead of a link-time crash with a stack trace.
|
|
19
|
+
*
|
|
20
|
+
* Compatibility is deliberately shape-based, not version-based: js-yaml 3, 4
|
|
21
|
+
* and 5 all expose a `load(source)` that Lisa uses identically, and Lisa uses
|
|
22
|
+
* nothing else from the package. Anything exposing that function works; a
|
|
23
|
+
* future major that renames it fails with a message naming the problem.
|
|
24
|
+
* @module utils/yaml
|
|
25
|
+
*/
|
|
26
|
+
import * as jsYaml from "js-yaml";
|
|
27
|
+
import { LisaError } from "../errors/index.js";
|
|
28
|
+
/** Human-readable statement of what Lisa can drive. */
|
|
29
|
+
export const SUPPORTED_JS_YAML_DESCRIPTION = "js-yaml 3.x, 4.x and 5.x (any build exposing a load() function)";
|
|
30
|
+
/**
|
|
31
|
+
* Raised when the resolved js-yaml cannot be driven by Lisa.
|
|
32
|
+
*
|
|
33
|
+
* Carries the remediation inline: the operator reading this in a postinstall
|
|
34
|
+
* warning has no other context to work from.
|
|
35
|
+
*/
|
|
36
|
+
export class IncompatibleYamlError extends LisaError {
|
|
37
|
+
/**
|
|
38
|
+
* Build the operator-facing incompatibility message.
|
|
39
|
+
* @param shape - Short description of what the resolved module did expose
|
|
40
|
+
*/
|
|
41
|
+
constructor(shape) {
|
|
42
|
+
super("Lisa cannot use the installed js-yaml: the resolved package exposes no " +
|
|
43
|
+
`load() function (${shape}). Lisa supports ${SUPPORTED_JS_YAML_DESCRIPTION}. ` +
|
|
44
|
+
"This usually means a js-yaml pin, override, or resolution in your " +
|
|
45
|
+
"package.json forced Lisa onto an incompatible copy. Remove or widen " +
|
|
46
|
+
"that js-yaml entry, reinstall, and re-run `lisa doctor`.", "INCOMPATIBLE_JS_YAML");
|
|
47
|
+
this.name = "IncompatibleYamlError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Pull a callable `load` off a candidate object.
|
|
52
|
+
* @param candidate - Namespace object, interop default, or anything else
|
|
53
|
+
* @returns The `load` function when present and callable
|
|
54
|
+
*/
|
|
55
|
+
function pickLoad(candidate) {
|
|
56
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const { load } = candidate;
|
|
60
|
+
return typeof load === "function" ? load : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Describe what a resolved module actually exposed, for the failure message.
|
|
64
|
+
* @param moduleNamespace - The resolved js-yaml module namespace
|
|
65
|
+
* @returns A short, bounded description of its exports
|
|
66
|
+
*/
|
|
67
|
+
function describeShape(moduleNamespace) {
|
|
68
|
+
if (typeof moduleNamespace !== "object" || moduleNamespace === null) {
|
|
69
|
+
return `resolved to ${typeof moduleNamespace}`;
|
|
70
|
+
}
|
|
71
|
+
const keys = Object.keys(moduleNamespace);
|
|
72
|
+
return keys.length === 0
|
|
73
|
+
? "no exports"
|
|
74
|
+
: `exports: ${keys.slice(0, 8).join(", ")}${keys.length > 8 ? ", ..." : ""}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve Lisa's YAML surface from a js-yaml module namespace, tolerating both
|
|
78
|
+
* the named-export shape (3.x/4.x/5.x ESM) and the CJS interop shape where the
|
|
79
|
+
* whole module hangs off `default`.
|
|
80
|
+
*
|
|
81
|
+
* Exported so `lisa doctor` can probe compatibility without parsing anything,
|
|
82
|
+
* and so the failure path is directly testable.
|
|
83
|
+
* @param moduleNamespace - The resolved js-yaml module namespace
|
|
84
|
+
* @returns The YAML surface Lisa uses
|
|
85
|
+
* @throws IncompatibleYamlError when no callable `load` can be found
|
|
86
|
+
*/
|
|
87
|
+
export function resolveYamlApi(moduleNamespace) {
|
|
88
|
+
const named = pickLoad(moduleNamespace);
|
|
89
|
+
if (named) {
|
|
90
|
+
return { load: named };
|
|
91
|
+
}
|
|
92
|
+
const interop = pickLoad(moduleNamespace?.default);
|
|
93
|
+
if (interop) {
|
|
94
|
+
return { load: interop };
|
|
95
|
+
}
|
|
96
|
+
throw new IncompatibleYamlError(describeShape(moduleNamespace));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse a single YAML document using whichever js-yaml the install resolved.
|
|
100
|
+
* @param source - YAML text
|
|
101
|
+
* @returns The parsed document
|
|
102
|
+
* @throws IncompatibleYamlError when the resolved js-yaml is unusable
|
|
103
|
+
*/
|
|
104
|
+
export function loadYaml(source) {
|
|
105
|
+
return resolveYamlApi(jsYaml).load(source);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Probe the js-yaml this process actually resolved.
|
|
109
|
+
*
|
|
110
|
+
* Never throws: `lisa doctor` turns the result into a check line, and a doctor
|
|
111
|
+
* that crashed on the very incompatibility it is meant to report would be the
|
|
112
|
+
* silent-failure bug all over again.
|
|
113
|
+
* @returns The incompatibility message, or null when the resolved js-yaml works
|
|
114
|
+
*/
|
|
115
|
+
export function probeYamlRuntime() {
|
|
116
|
+
try {
|
|
117
|
+
resolveYamlApi(jsYaml);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return error instanceof Error ? error.message : String(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=yaml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml.js","sourceRoot":"","sources":["../../src/utils/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,MAAM,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAY/C,uDAAuD;AACvD,MAAM,CAAC,MAAM,6BAA6B,GACxC,iEAAiE,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,OAAO,qBAAsB,SAAQ,SAAS;IAClD;;;OAGG;IACH,YAAY,KAAa;QACvB,KAAK,CACH,yEAAyE;YACvE,oBAAoB,KAAK,oBAAoB,6BAA6B,IAAI;YAC9E,oEAAoE;YACpE,sEAAsE;YACtE,0DAA0D,EAC5D,sBAAsB,CACvB,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,SAAkB;IAClC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,GAAG,SAAwC,CAAC;IAC1D,OAAO,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAE,IAAwB,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,eAAwB;IAC7C,IAAI,OAAO,eAAe,KAAK,QAAQ,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QACpE,OAAO,eAAe,OAAO,eAAe,EAAE,CAAC;IACjD,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC;QACtB,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,eAAwB;IACrD,MAAM,KAAK,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CACrB,eAAyD,EAAE,OAAO,CACpE,CAAC;IACF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC;IACD,MAAM,IAAI,qBAAqB,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC;QACH,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
* @module scripts/bdd/baseline
|
|
28
28
|
*/
|
|
29
29
|
import { spawnSync } from "node:child_process";
|
|
30
|
+
import * as fs from "node:fs";
|
|
31
|
+
import * as path from "node:path";
|
|
30
32
|
|
|
31
|
-
import { declaredPlatforms } from "./contract.mjs";
|
|
33
|
+
import { byCodeUnit, declaredPlatforms } from "./contract.mjs";
|
|
32
34
|
import { parseFeatureSource, scenarioIdsIn } from "./parse.mjs";
|
|
33
35
|
|
|
34
36
|
const defect = (code, message) => ({ code, message });
|
|
@@ -36,6 +38,77 @@ const defect = (code, message) => ({ code, message });
|
|
|
36
38
|
/** The maintainer-applied PR label that authorizes giving coverage back. */
|
|
37
39
|
export const BASELINE_LABEL = "bdd-floor-baseline";
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Fixed absolute locations git is installed at, tried before anything on PATH.
|
|
43
|
+
*
|
|
44
|
+
* Handing a bare command name to the process spawner delegates the choice of
|
|
45
|
+
* binary to whatever `PATH` happens to say, so any writable directory earlier
|
|
46
|
+
* on it decides what this gate executes. The gate reads a base revision to
|
|
47
|
+
* decide whether a pull request may merge, which makes that a real
|
|
48
|
+
* substitution risk and not a theoretical one.
|
|
49
|
+
*/
|
|
50
|
+
const GIT_CANDIDATES = Object.freeze([
|
|
51
|
+
"/usr/bin/git",
|
|
52
|
+
"/usr/local/bin/git",
|
|
53
|
+
"/opt/homebrew/bin/git",
|
|
54
|
+
"/bin/git",
|
|
55
|
+
"C:\\Program Files\\Git\\cmd\\git.exe",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** Resolved git path, or null; computed once per process. */
|
|
59
|
+
let gitBinary;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a path names a file that exists.
|
|
63
|
+
* @param {string} candidate - Absolute path.
|
|
64
|
+
* @returns {boolean} Whether it is a regular file.
|
|
65
|
+
*/
|
|
66
|
+
function isFile(candidate) {
|
|
67
|
+
try {
|
|
68
|
+
return fs.statSync(candidate).isFile();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve git to an ABSOLUTE, verified path.
|
|
76
|
+
*
|
|
77
|
+
* Fixed system locations first, then each `PATH` entry resolved to an absolute
|
|
78
|
+
* path and confirmed to be a real file — a lookup, never a delegation, so the
|
|
79
|
+
* command actually executed is one this process chose and checked.
|
|
80
|
+
* @returns {string|null} The absolute path, or null when git is not installed.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveGit() {
|
|
83
|
+
if (gitBinary !== undefined) return gitBinary;
|
|
84
|
+
const fromPath = (process.env.PATH ?? "")
|
|
85
|
+
.split(path.delimiter)
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.flatMap(entry => ["git", "git.exe"].map(name => path.join(entry, name)))
|
|
88
|
+
.filter(candidate => path.isAbsolute(candidate));
|
|
89
|
+
gitBinary =
|
|
90
|
+
[...GIT_CANDIDATES, ...fromPath].find(candidate => isFile(candidate)) ??
|
|
91
|
+
null;
|
|
92
|
+
return gitBinary;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run one git command at an absolute path, or report that git is unavailable.
|
|
97
|
+
* @param {string} root - Repo root.
|
|
98
|
+
* @param {readonly string[]} args - Arguments after the binary.
|
|
99
|
+
* @returns {{ok: boolean, stdout: string}} The verdict and its output.
|
|
100
|
+
*/
|
|
101
|
+
function git(root, args) {
|
|
102
|
+
const binary = resolveGit();
|
|
103
|
+
if (!binary) return { ok: false, stdout: "" };
|
|
104
|
+
const result = spawnSync(binary, [...args], {
|
|
105
|
+
cwd: root,
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
108
|
+
});
|
|
109
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "" };
|
|
110
|
+
}
|
|
111
|
+
|
|
39
112
|
/**
|
|
40
113
|
* Read one path at a git revision.
|
|
41
114
|
* @param {string} root - Repo root.
|
|
@@ -44,12 +117,8 @@ export const BASELINE_LABEL = "bdd-floor-baseline";
|
|
|
44
117
|
* @returns {string|null} File contents, or null when absent at that revision.
|
|
45
118
|
*/
|
|
46
119
|
export function showAtRevision(root, revision, relative) {
|
|
47
|
-
const result =
|
|
48
|
-
|
|
49
|
-
encoding: "utf8",
|
|
50
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
51
|
-
});
|
|
52
|
-
return result.status === 0 ? result.stdout : null;
|
|
120
|
+
const result = git(root, ["show", `${revision}:${relative}`]);
|
|
121
|
+
return result.ok ? result.stdout : null;
|
|
53
122
|
}
|
|
54
123
|
|
|
55
124
|
/**
|
|
@@ -59,12 +128,15 @@ export function showAtRevision(root, revision, relative) {
|
|
|
59
128
|
* @returns {string[]} Repo-relative feature paths.
|
|
60
129
|
*/
|
|
61
130
|
function featureFilesAt(root, revision) {
|
|
62
|
-
const result =
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
131
|
+
const result = git(root, [
|
|
132
|
+
"ls-tree",
|
|
133
|
+
"-r",
|
|
134
|
+
"--name-only",
|
|
135
|
+
revision,
|
|
136
|
+
"--",
|
|
137
|
+
"bdd/features",
|
|
138
|
+
]);
|
|
139
|
+
if (!result.ok) return [];
|
|
68
140
|
return result.stdout
|
|
69
141
|
.split("\n")
|
|
70
142
|
.map(line => line.trim())
|
|
@@ -79,21 +151,33 @@ function featureFilesAt(root, revision) {
|
|
|
79
151
|
* an unknown tag the moment a pull request deleted the web runner from
|
|
80
152
|
* `runnerPlatforms` — which would turn "I deleted the runner that proved this"
|
|
81
153
|
* into an invisible change rather than the coverage loss it is.
|
|
154
|
+
*
|
|
155
|
+
* A base coverage map that EXISTS but does not parse makes the baseline
|
|
156
|
+
* UNAVAILABLE rather than empty. An empty `before` set produces no
|
|
157
|
+
* `coverage-regression` and no `scenario-deleted` finding at all, so treating
|
|
158
|
+
* an unreadable map as "nothing was covered before" would let one malformed
|
|
159
|
+
* character on the base revision switch off every non-regression check while
|
|
160
|
+
* the gate still reported that nothing regressed.
|
|
82
161
|
* @param {string} root - Repo root.
|
|
83
162
|
* @param {string} revision - Base commit-ish.
|
|
84
163
|
* @param {ReadonlySet<string>} headPlatforms - The head contract's platforms.
|
|
85
|
-
* @returns {{available: boolean, contract: object|null, scenarios: object[], scenarioIds: Set<string>}} Base state.
|
|
164
|
+
* @returns {{available: boolean, error: string|null, contract: object|null, scenarios: object[], scenarioIds: Set<string>}} Base state.
|
|
86
165
|
*/
|
|
87
166
|
export function loadBaseline(root, revision, headPlatforms = new Set()) {
|
|
88
167
|
const raw = showAtRevision(root, revision, "bdd/coverage-map.json");
|
|
89
168
|
let contract = null;
|
|
169
|
+
let error = null;
|
|
90
170
|
if (raw !== null) {
|
|
91
171
|
try {
|
|
92
172
|
contract = JSON.parse(raw);
|
|
93
|
-
} catch {
|
|
173
|
+
} catch (parseError) {
|
|
94
174
|
contract = null;
|
|
175
|
+
error = `bdd/coverage-map.json at that revision is not valid JSON: ${parseError.message}`;
|
|
95
176
|
}
|
|
96
177
|
}
|
|
178
|
+
if (raw === null && !resolveGit()) {
|
|
179
|
+
error = "git was not found, so no base revision could be read";
|
|
180
|
+
}
|
|
97
181
|
const documents = featureFilesAt(root, revision)
|
|
98
182
|
.map(file => ({ file, source: showAtRevision(root, revision, file) }))
|
|
99
183
|
.filter(document => document.source !== null);
|
|
@@ -102,7 +186,8 @@ export function loadBaseline(root, revision, headPlatforms = new Set()) {
|
|
|
102
186
|
...headPlatforms,
|
|
103
187
|
]);
|
|
104
188
|
return {
|
|
105
|
-
available: raw !== null || documents.length > 0,
|
|
189
|
+
available: error === null && (raw !== null || documents.length > 0),
|
|
190
|
+
error,
|
|
106
191
|
contract,
|
|
107
192
|
scenarios: documents.flatMap(document =>
|
|
108
193
|
parseFeatureSource(document.source, document.file, platforms)
|
|
@@ -230,7 +315,7 @@ export function checkCoverageRegression({
|
|
|
230
315
|
const waived = waivedKeys(contract);
|
|
231
316
|
return [...before]
|
|
232
317
|
.filter(key => !after.has(key) && present.has(partsOf(key).scenario))
|
|
233
|
-
.sort()
|
|
318
|
+
.sort(byCodeUnit)
|
|
234
319
|
.flatMap(key => regressionDefect(key, { retired, waived, labels }));
|
|
235
320
|
}
|
|
236
321
|
|
|
@@ -291,7 +376,7 @@ export function checkNewObligations({ baseline, contract, scenarios }) {
|
|
|
291
376
|
const covered = acceptedKeys(scenarios, contract);
|
|
292
377
|
return [...obligationKeys(scenarios, contract)]
|
|
293
378
|
.filter(key => !before.has(key) && !covered.has(key))
|
|
294
|
-
.sort()
|
|
379
|
+
.sort(byCodeUnit)
|
|
295
380
|
.map(key => {
|
|
296
381
|
const { scenario, platform } = partsOf(key);
|
|
297
382
|
return defect(
|
|
@@ -317,7 +402,7 @@ export function checkDeletions({ baseIds, scenarios, contract, labels }) {
|
|
|
317
402
|
);
|
|
318
403
|
return [...baseIds]
|
|
319
404
|
.filter(id => !present.has(id))
|
|
320
|
-
.sort()
|
|
405
|
+
.sort(byCodeUnit)
|
|
321
406
|
.flatMap(id => deletionDefects(id, retired.get(id), labels));
|
|
322
407
|
}
|
|
323
408
|
|
|
@@ -20,6 +20,25 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export const REPORT_SCHEMA_VERSION = 3;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* THE comparator every ordering in this gate uses.
|
|
25
|
+
*
|
|
26
|
+
* `Array.prototype.sort()` with no comparator coerces each element to a string
|
|
27
|
+
* and compares UTF-16 code units, which is right for the identifier lists this
|
|
28
|
+
* gate sorts and catastrophically wrong the first time someone sorts numbers
|
|
29
|
+
* (`[2, 10]` becomes `[10, 2]`). Stating the comparator makes that choice
|
|
30
|
+
* deliberate rather than inherited, and it reproduces the previous ordering
|
|
31
|
+
* exactly — deliberately NOT `localeCompare`, whose result depends on the
|
|
32
|
+
* runner's ICU data and would reorder committed report artifacts.
|
|
33
|
+
* @param {string} a - Left value.
|
|
34
|
+
* @param {string} b - Right value.
|
|
35
|
+
* @returns {number} Negative, zero, or positive.
|
|
36
|
+
*/
|
|
37
|
+
export const byCodeUnit = (a, b) => {
|
|
38
|
+
if (a < b) return -1;
|
|
39
|
+
return a > b ? 1 : 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
23
42
|
/** Coverage-map schema versions this gate can read. */
|
|
24
43
|
export const SUPPORTED_MAP_SCHEMA_VERSIONS = [1, 2];
|
|
25
44
|
|
|
@@ -115,7 +134,7 @@ export function runnersByPlatform(runnerPlatforms) {
|
|
|
115
134
|
index.set(platform, [...(index.get(platform) ?? []), runner]);
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
|
-
for (const runners of index.values()) runners.sort();
|
|
137
|
+
for (const runners of index.values()) runners.sort(byCodeUnit);
|
|
119
138
|
return index;
|
|
120
139
|
}
|
|
121
140
|
|
|
@@ -20,8 +20,43 @@
|
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
21
|
import * as path from "node:path";
|
|
22
22
|
|
|
23
|
+
import { byCodeUnit } from "./contract.mjs";
|
|
23
24
|
import { listFiles, posix, resolveInsideRepo } from "./parse.mjs";
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a repo-relative path or path prefix to one comparable spelling.
|
|
28
|
+
*
|
|
29
|
+
* `./e2e/`, `e2e`, and `e2e/` all name the same directory, and the repo root
|
|
30
|
+
* is spelled `.` in a `roots` list but never appears in a repo-relative file
|
|
31
|
+
* path at all. Comparing the raw strings made those spellings disagree.
|
|
32
|
+
* @param {string} value - A repo-relative path or prefix.
|
|
33
|
+
* @returns {string} Its canonical form; the empty string means the repo root.
|
|
34
|
+
*/
|
|
35
|
+
const canonicalPath = value =>
|
|
36
|
+
posix(String(value))
|
|
37
|
+
.replace(/^\.\/+/, "")
|
|
38
|
+
.replace(/\/+$/, "")
|
|
39
|
+
.replace(/^\.$/, "");
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a repo-relative path lies at or under a path prefix, SEGMENT-WISE.
|
|
43
|
+
*
|
|
44
|
+
* A raw `startsWith` treats a prefix as a character prefix, so an ignore entry
|
|
45
|
+
* of `e2e/live` also swallowed `e2e/live-personas/…` — silently hiding a whole
|
|
46
|
+
* directory of undeclared tests, which is the one thing discovery exists to
|
|
47
|
+
* find. Matching is therefore on path segments: `e2e/live` covers `e2e/live`
|
|
48
|
+
* and `e2e/live/…` and nothing else.
|
|
49
|
+
* @param {string} relative - A repo-relative path.
|
|
50
|
+
* @param {string} prefix - A repo-relative path prefix.
|
|
51
|
+
* @returns {boolean} Whether the path is covered by the prefix.
|
|
52
|
+
*/
|
|
53
|
+
export function isUnderPrefix(relative, prefix) {
|
|
54
|
+
const target = canonicalPath(relative);
|
|
55
|
+
const root = canonicalPath(prefix);
|
|
56
|
+
if (root === "") return true;
|
|
57
|
+
return target === root || target.startsWith(`${root}/`);
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
/**
|
|
26
61
|
* Build one defect record, always naming the entity it is about.
|
|
27
62
|
* @param {string} code - Stable machine-readable defect code.
|
|
@@ -213,12 +248,18 @@ function callTitles(source, functions) {
|
|
|
213
248
|
);
|
|
214
249
|
const found = [];
|
|
215
250
|
for (const match of source.matchAll(pattern)) {
|
|
251
|
+
// Exactly one of the three quoting alternatives participates in any match,
|
|
252
|
+
// so the others are genuinely `undefined` here. The tests are written by
|
|
253
|
+
// TYPE rather than against `undefined`: the intent is "did this call carry
|
|
254
|
+
// a usable title" and "did that title come from a template literal", and
|
|
255
|
+
// stating it that way is both what the code means and unambiguous to a
|
|
256
|
+
// static analyser reading the declared `string` element type.
|
|
216
257
|
const template = match[3];
|
|
217
258
|
const evidence = match[1] ?? match[2] ?? template;
|
|
218
|
-
if (evidence
|
|
259
|
+
if (typeof evidence !== "string" || evidence.length === 0) continue;
|
|
219
260
|
found.push({
|
|
220
261
|
evidence,
|
|
221
|
-
dynamic: template
|
|
262
|
+
dynamic: typeof template === "string" && template.includes("${"),
|
|
222
263
|
});
|
|
223
264
|
}
|
|
224
265
|
return found;
|
|
@@ -247,13 +288,15 @@ function lineField(source, field) {
|
|
|
247
288
|
* @returns {object[]} Discovered specs.
|
|
248
289
|
*/
|
|
249
290
|
function discoverRunner({ root, runner, config, contract }) {
|
|
250
|
-
const platforms = [...(contract.runnerPlatforms?.[runner] ?? [])].sort(
|
|
291
|
+
const platforms = [...(contract.runnerPlatforms?.[runner] ?? [])].sort(
|
|
292
|
+
byCodeUnit
|
|
293
|
+
);
|
|
251
294
|
const ignore = config.ignore ?? [];
|
|
252
295
|
const specs = [];
|
|
253
296
|
for (const declaredRoot of config.roots) {
|
|
254
297
|
for (const file of filesUnder(root, declaredRoot, config)) {
|
|
255
298
|
const relative = posix(path.relative(root, file));
|
|
256
|
-
if (ignore.some(prefix => relative
|
|
299
|
+
if (ignore.some(prefix => isUnderPrefix(relative, prefix))) continue;
|
|
257
300
|
const source = fs.readFileSync(file, "utf8");
|
|
258
301
|
for (const found of extract(source, config.evidence)) {
|
|
259
302
|
specs.push({ runner, platforms, file: relative, ...found });
|
|
@@ -306,7 +349,7 @@ export function discoverSpecs({ root, contract }) {
|
|
|
306
349
|
const specs = [];
|
|
307
350
|
const roots = [];
|
|
308
351
|
for (const [runner, config] of configs) {
|
|
309
|
-
roots.push(...config.roots.map(
|
|
352
|
+
roots.push(...config.roots.map(canonicalPath));
|
|
310
353
|
specs.push(...discoverRunner({ root, runner, config, contract }));
|
|
311
354
|
}
|
|
312
355
|
return {
|
|
@@ -315,8 +358,8 @@ export function discoverSpecs({ root, contract }) {
|
|
|
315
358
|
`${b.file}${b.evidence ?? ""}`
|
|
316
359
|
)
|
|
317
360
|
),
|
|
318
|
-
runners: [...configs.keys()].sort(),
|
|
319
|
-
roots: [...new Set(roots)].sort(),
|
|
361
|
+
runners: [...configs.keys()].sort(byCodeUnit),
|
|
362
|
+
roots: [...new Set(roots)].sort(byCodeUnit),
|
|
320
363
|
defects,
|
|
321
364
|
};
|
|
322
365
|
}
|
|
@@ -445,11 +488,11 @@ function exclusionStale({ root, exclusion, discovery, at, subject }) {
|
|
|
445
488
|
),
|
|
446
489
|
];
|
|
447
490
|
}
|
|
448
|
-
if (!discovery.roots.some(entry => exclusion.file
|
|
491
|
+
if (!discovery.roots.some(entry => isUnderPrefix(exclusion.file, entry))) {
|
|
449
492
|
return [
|
|
450
493
|
defect(
|
|
451
494
|
"exclusion-stale",
|
|
452
|
-
`${at}: ${exclusion.file} is covered by no configured discovery root (${discovery.roots.join(", ") || "none declared"}), so the exclusion suppresses nothing`,
|
|
495
|
+
`${at}: ${exclusion.file} is covered by no configured discovery root (${discovery.roots.map(entry => entry || ".").join(", ") || "none declared"}), so the exclusion suppresses nothing`,
|
|
453
496
|
subject
|
|
454
497
|
),
|
|
455
498
|
];
|
|
@@ -491,7 +534,7 @@ export function missingDiscoveryDefects(contract, discovery) {
|
|
|
491
534
|
return Object.keys(contract.runnerPlatforms ?? {})
|
|
492
535
|
.filter(runner => !runner.startsWith("_"))
|
|
493
536
|
.filter(runner => !discovery.runners.includes(runner))
|
|
494
|
-
.sort()
|
|
537
|
+
.sort(byCodeUnit)
|
|
495
538
|
.map(runner =>
|
|
496
539
|
defect(
|
|
497
540
|
"discovery-missing",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ID_PATTERN,
|
|
15
15
|
LIFECYCLE_TAGS,
|
|
16
16
|
PROVENANCE_PATTERN,
|
|
17
|
+
byCodeUnit,
|
|
17
18
|
parseTrackerTag,
|
|
18
19
|
} from "./contract.mjs";
|
|
19
20
|
|
|
@@ -87,7 +88,7 @@ export function listFiles(directory, predicate) {
|
|
|
87
88
|
else if (entry.isFile() && predicate(full)) found.push(full);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
return found.sort();
|
|
91
|
+
return found.sort(byCodeUnit);
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
@@ -130,6 +131,12 @@ function categorize(tags, platforms) {
|
|
|
130
131
|
* Tags accumulate until a `Feature:` or `Scenario:` consumes them, matching
|
|
131
132
|
* Gherkin's own tag scoping. `Scenario Outline` is treated as one scenario:
|
|
132
133
|
* the contract counts behaviors, not example rows.
|
|
134
|
+
*
|
|
135
|
+
* `Background:` steps are Gherkin's way of saying "every scenario in this
|
|
136
|
+
* feature starts here", so they SEED each scenario's primary steps. Ignoring
|
|
137
|
+
* them made a conforming feature file — Given in the Background, When/Then in
|
|
138
|
+
* each scenario — report `scenario-steps` defects it did not have, which in
|
|
139
|
+
* enforced mode fails a repo for writing correct Gherkin.
|
|
133
140
|
* @param {string} source - File contents.
|
|
134
141
|
* @param {string} file - Repo-relative path, for error locations.
|
|
135
142
|
* @param {ReadonlySet<string>} platforms - The project's declared platform vocabulary.
|
|
@@ -144,6 +151,8 @@ export function parseFeatureSource(source, file, platforms) {
|
|
|
144
151
|
let featureLine = 0;
|
|
145
152
|
let pending = [];
|
|
146
153
|
let current = null;
|
|
154
|
+
let background = null;
|
|
155
|
+
let backgroundSteps = [];
|
|
147
156
|
for (let index = 0; index < lines.length; index += 1) {
|
|
148
157
|
const trimmed = lines[index].trim();
|
|
149
158
|
if (trimmed.startsWith("#")) continue;
|
|
@@ -165,6 +174,16 @@ export function parseFeatureSource(source, file, platforms) {
|
|
|
165
174
|
featureLine = index + 1;
|
|
166
175
|
pending = [];
|
|
167
176
|
current = null;
|
|
177
|
+
background = null;
|
|
178
|
+
backgroundSteps = [];
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (/^Background:/.test(trimmed)) {
|
|
182
|
+
// Background is per-Feature, so it replaces any previous one rather than
|
|
183
|
+
// accumulating, and its steps are collected instead of a scenario's.
|
|
184
|
+
backgroundSteps = [];
|
|
185
|
+
background = backgroundSteps;
|
|
186
|
+
current = null;
|
|
168
187
|
continue;
|
|
169
188
|
}
|
|
170
189
|
const scenarioMatch = /^Scenario(?: Outline| Template)?:\s*(.+)$/.exec(
|
|
@@ -189,14 +208,17 @@ export function parseFeatureSource(source, file, platforms) {
|
|
|
189
208
|
featureLine,
|
|
190
209
|
...grouped,
|
|
191
210
|
required: grouped.lifecycle.length === 0,
|
|
192
|
-
primarySteps: [],
|
|
211
|
+
primarySteps: [...backgroundSteps],
|
|
193
212
|
};
|
|
194
213
|
scenarios.push(current);
|
|
214
|
+
background = null;
|
|
195
215
|
pending = [];
|
|
196
216
|
continue;
|
|
197
217
|
}
|
|
198
218
|
const step = /^(Given|When|Then)\b/.exec(trimmed);
|
|
199
|
-
if (step
|
|
219
|
+
if (!step) continue;
|
|
220
|
+
if (background) background.push(step[1]);
|
|
221
|
+
else if (current) current.primarySteps.push(step[1]);
|
|
200
222
|
}
|
|
201
223
|
return scenarios;
|
|
202
224
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module scripts/bdd/render
|
|
9
9
|
*/
|
|
10
|
+
import { byCodeUnit } from "./contract.mjs";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Format a coverage figure, refusing to print "100%" for an empty
|
|
@@ -175,7 +176,7 @@ function gapSections(report) {
|
|
|
175
176
|
const rows = [...entries.entries()]
|
|
176
177
|
.map(
|
|
177
178
|
([id, value]) =>
|
|
178
|
-
`| ${cell(id)} | ${cell(value.name)} | ${cell(value.platforms.sort().join(", "))} |`
|
|
179
|
+
`| ${cell(id)} | ${cell(value.name)} | ${cell([...value.platforms].sort(byCodeUnit).join(", "))} |`
|
|
179
180
|
)
|
|
180
181
|
.join("\n");
|
|
181
182
|
return `### ${cell(feature)}\n\n| Scenario | Behavior with no mapped proof | Platforms |\n|---|---|---|\n${rows}`;
|