rcf-lite 0.28.0 → 0.28.1
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/CHANGELOG.md +16 -0
- package/README.md +9 -0
- package/fixtures/canary-manifest.json +6 -6
- package/package.json +1 -1
- package/rcf/code-nodes/cn-600.json +2 -1
- package/rcf/code-nodes/cn-602.json +3 -2
- package/rcf/code-nodes/cn-605.json +2 -1
- package/rcf/fbs/fbs-181.json +2 -1
- package/rcf/fbs/fbs-182.json +2 -1
- package/rcf/tacs/tac-4103-feedback-redact.json +3 -2
- package/rcf/tacs/tac-4106-feedback-destination.json +3 -2
- package/rcf/test-suites/ts-188.json +10 -2
- package/rcf/test-suites/ts-189.json +24 -2
- package/rcf/user-stories/us-15601.json +10 -1
- package/rcf/user-stories/us-15801.json +10 -1
- package/releases/releases.yaml +11 -1
- package/src/cli/feedback.js +11 -2
- package/src/feedback/destination.js +98 -4
- package/src/feedback/redact.js +44 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.28.1] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
Feedback defects surfaced by the first WSD-lens round-trip on 0.28.0. Fixes both a destination bug that silently misrouted library-owned findings to `Stravica/rcf-lite`, and a redactor over-fold that mangled the docs URL and any prose using the canonical noun `agent`. Includes a package-rename disclosure banner in the README so a colleague hitting `npm i -g @stravica-ai/rcf-build-lite` on the retired scoped name knows what to do.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Feedback destination for bare-slug library-owned blueprints (AC-15801-4).** A `--kind blueprint` entry whose manifest record has no `libraryPrefix` now cross-checks the blueprint-library registry BEFORE the shelf-constant fall-through: if exactly one registered library owns a blueprint with the applied slug AND that library resolves to a destination (`issuesRepo`, or a derivable `git+https://github.com/...` sourceRef), the resolver routes to that library's destination with `source: 'library-slug-inference'`. Two or more owners return `unresolved` with `reason: 'ambiguous-library-slug'` and a `candidates[]` array of library prefixes. Zero owners keep the shelf-constant fall-through unchanged. This never overrides an existing `libraryPrefix` resolution (AC-15801-2's path is untouched). Before 0.28.1, a WSD colleague who ran `rcf define blueprint add application-spa` from a project with a registered WSD library that also owned `application-spa` saw their feedback routed to `Stravica/rcf-lite` instead of the WSD blueprints repo the library declared. Chain: US-15801 AC-15801-4; design amendment R5a; TS-189 gains three test cases; TAC-4106 responsibility extended.
|
|
16
|
+
|
|
17
|
+
- **Operator-identity redactor no longer folds the canonical noun `agent` (AC-15601-5).** The operator-identity substitution now consults a `RESERVED_IDENTITY_TOKENS` stopword set (agent, user, admin, api, code, system, and the rest of the guarded common nouns) before folding each whitespace-separated token of the identity `## Name`. Before 0.28.1, an identity such as `The Agent Alice` caused the redactor to fold the literal `agent` throughout the body: the docs URL `how-the-agent-files-feedback` became `how-the-<operator>-files-feedback` (no longer resolvable), the canon phrase `when the agent stops` mangled, and every RULE 17 example was affected. Real given names (Alice, Smith) still fold; only common-noun tokens are guarded. When the identity string is ENTIRELY stopwords (a placeholder in disguise), the full-literal substitution is also skipped so the docs and canon phrases survive. Chain: US-15601 AC-15601-5; design amendment R5c; TS-188 gains one test case; TAC-4103 responsibility extended.
|
|
18
|
+
|
|
19
|
+
- **`rcf feedback add --evidence` help text now names its requiredness explicitly.** Header line `'rcf feedback add' options (all required unless noted)` was preserved and truthful, but `--evidence <pointer>` did not itemise its required-and-repeatable behaviour, so a reader could take the line-item shape alone as "optional." Help text now reads `Required, repeatable: a command, a path:line inside the project, or an id. At least one --evidence is required (AC-15501-1); absence exits 2.` No behaviour change; AC-15501-1 already required at least one `--evidence`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Package rename disclosure surfaced in the README and CHANGELOG.** The scoped name `@stravica-ai/rcf-build-lite` was frozen at 0.7.0 and the suite consolidated into the unscoped `rcf-lite` at 0.7.1; the 0.28.0 line ships ONLY on `rcf-lite`. The README already carried the rename note under "Where this came from" and pointed at the packaging runbook, but there was no in-CLI hint: a colleague running `npm i -g @stravica-ai/rcf-build-lite@0.28.0` saw a bare "no matching version" from npm with no rcf-lite context. 0.28.1 adds a callout box at the top of the README naming the rename explicitly and the CHANGELOG carries this note so a search for the retired name lands somewhere useful. The npm-side deprecate notice on the retired scoped name is a Dave operator step (needs npm auth) tracked separately from this release. No behaviour change.
|
|
24
|
+
|
|
9
25
|
## [0.28.0] - 2026-09-17
|
|
10
26
|
|
|
11
27
|
rcf-lite 0.28.0 lands two referee-facing trains together. The `rcf feedback` verb family lets an agent record what a blueprint or rcf-lite did that contradicts its own docs, fails, or forces a workaround, redact it to the eight rules of the design plus a whole-body residual scan, and, after one operator yes at a natural pause, file each finding as a GitHub issue under the operator's own `gh` login (or a pastable outbox bundle on the fallback path). The referee-guarantees train tightens the three sentences the docs page makes about `rcf define validate`, `rcf audit coverage` and `rcf build finalise`: validate now walks ADR `supersededBy`, coverage is strict-by-default with `--mode shallow-any` as the deliberate opt-out, and finalise refuses on the three per-AC verdicts (UI-BASELINE-UNMET, BROWSER-VERIFICATION-MISSING, SCOPE-MISMATCH), gates the manual `verified` override behind `--acknowledge-verified-override`, and adds a merge-state precondition with `--allow-pre-merge` as the deliberate pre-merge dry-run opt-out. `rcf init` now merges three hook entries into `.claude/settings.json` and `.codex/hooks.json` so the feedback ask fires once per session at idle; RULE 17 lands in the managed instructions block as the belt-and-braces path. Coverage-strict-by-default is the single migration surface: downstream projects that ran plain `rcf audit coverage` and treated its exit code as passing will see the same tree exit 4; the fix is either to close the coverage gap or to run `rcf audit coverage --mode shallow-any` when the intent is a printed gap report.
|
package/README.md
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
Build software with an AI coding agent without losing the plot.
|
|
9
9
|
|
|
10
|
+
> **Package name note (0.7.1+).** This tool ships as `rcf-lite` on npm. The retired scoped name `@stravica-ai/rcf-build-lite` was frozen at 0.7.0 in August 2026 and every release from 0.7.1 onwards lives here. If `npm install -g @stravica-ai/rcf-build-lite@<version>` returns "no matching version" for any version above 0.7.0, uninstall the scoped name and install `rcf-lite` at the same version instead:
|
|
11
|
+
>
|
|
12
|
+
> ```sh
|
|
13
|
+
> npm uninstall -g @stravica-ai/rcf-build-lite
|
|
14
|
+
> npm install -g rcf-lite
|
|
15
|
+
> ```
|
|
16
|
+
>
|
|
17
|
+
> The `rcf` and `rcf-verify` bin names are unchanged.
|
|
18
|
+
|
|
10
19
|
Anyone who has shipped with a coding agent knows the failure mode: the code arrives fast, but what the product is supposed to do lives in prompt history, and prompt history is not a spec. Three weeks later nobody can say what is covered, what is tested, or what breaks when something changes.
|
|
11
20
|
|
|
12
21
|
RCF Lite keeps those answers machine-checkable. It maintains a live chain from what you asked for, through requirements, user stories, acceptance criteria and tests, into the code itself, as plain JSON files in your own repository. Your agent works the chain instead of improvising; `rcf define validate` catches drift the moment it happens; and when your app is deployed, `rcf verify` sends an independent verifier at it before anything gets called done.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"registerCanary": [
|
|
3
3
|
{
|
|
4
4
|
"id": "rc-2026-09-17-001",
|
|
5
|
-
"createdAt": "2026-09-
|
|
6
|
-
"buildVersion": "0.28.
|
|
5
|
+
"createdAt": "2026-09-17T14:42:00.660Z",
|
|
6
|
+
"buildVersion": "0.28.1-mockdriver",
|
|
7
7
|
"fixturePromptId": "canary-prompt-01",
|
|
8
8
|
"responseWordCount": 55,
|
|
9
9
|
"grades": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"id": "rc-2026-09-17-002",
|
|
38
|
-
"createdAt": "2026-09-
|
|
39
|
-
"buildVersion": "0.28.
|
|
38
|
+
"createdAt": "2026-09-17T14:42:00.661Z",
|
|
39
|
+
"buildVersion": "0.28.1-mockdriver",
|
|
40
40
|
"fixturePromptId": "canary-prompt-02",
|
|
41
41
|
"responseWordCount": 55,
|
|
42
42
|
"grades": {
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"id": "rc-2026-09-17-003",
|
|
71
|
-
"createdAt": "2026-09-
|
|
72
|
-
"buildVersion": "0.28.
|
|
71
|
+
"createdAt": "2026-09-17T14:42:00.661Z",
|
|
72
|
+
"buildVersion": "0.28.1-mockdriver",
|
|
73
73
|
"fixturePromptId": "canary-prompt-03",
|
|
74
74
|
"responseWordCount": 55,
|
|
75
75
|
"grades": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rcf-lite",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-install tooling for the Requirements Confidence Framework (RCF): the unified `rcf` CLI grouped into the five RCF tool groups (discover, define, build, verify, audit) plus a small core set (init, doctor, guidance, mcp), an MCP server, the live tree viewer and the fresh-context adversarial ship-gate verifier. Consumes @stravica-ai/rcf-schemas.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"AC-15601-4",
|
|
18
18
|
"AC-15701-2",
|
|
19
19
|
"AC-15701-3",
|
|
20
|
+
"AC-15801-4",
|
|
20
21
|
"AC-15901-1",
|
|
21
22
|
"AC-15901-2",
|
|
22
23
|
"AC-15902-1",
|
|
@@ -41,5 +42,5 @@
|
|
|
41
42
|
"version": "0.1.0",
|
|
42
43
|
"status": "draft",
|
|
43
44
|
"createdAt": "2026-09-16T14:58:28Z",
|
|
44
|
-
"updatedAt": "2026-09-
|
|
45
|
+
"updatedAt": "2026-09-17T14:14:49Z"
|
|
45
46
|
}
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
"AC-15601-1",
|
|
6
6
|
"AC-15601-2",
|
|
7
7
|
"AC-15601-3",
|
|
8
|
-
"AC-15601-4"
|
|
8
|
+
"AC-15601-4",
|
|
9
|
+
"AC-15601-5"
|
|
9
10
|
],
|
|
10
11
|
"dependencies": [],
|
|
11
12
|
"version": "0.1.0",
|
|
12
13
|
"status": "draft",
|
|
13
14
|
"createdAt": "2026-09-16T15:27:35Z",
|
|
14
|
-
"updatedAt": "2026-09-
|
|
15
|
+
"updatedAt": "2026-09-17T14:14:49Z"
|
|
15
16
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"implementsAcIds": [
|
|
5
5
|
"AC-15801-2",
|
|
6
6
|
"AC-15801-3",
|
|
7
|
+
"AC-15801-4",
|
|
7
8
|
"AC-15802-1",
|
|
8
9
|
"AC-15802-2"
|
|
9
10
|
],
|
|
@@ -13,5 +14,5 @@
|
|
|
13
14
|
"version": "0.1.0",
|
|
14
15
|
"status": "draft",
|
|
15
16
|
"createdAt": "2026-09-16T16:20:01Z",
|
|
16
|
-
"updatedAt": "2026-09-
|
|
17
|
+
"updatedAt": "2026-09-17T14:14:49Z"
|
|
17
18
|
}
|
package/rcf/fbs/fbs-181.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"AC-15601-2",
|
|
13
13
|
"AC-15601-3",
|
|
14
14
|
"AC-15601-4",
|
|
15
|
+
"AC-15601-5",
|
|
15
16
|
"AC-15701-1"
|
|
16
17
|
],
|
|
17
18
|
"dependsOnFbsIds": [
|
|
@@ -41,5 +42,5 @@
|
|
|
41
42
|
"estimatedHours": 16,
|
|
42
43
|
"notes": "Fingerprint decision confined to this slice via ADR-4102; the labels half of that ADR pays off in slice 4.",
|
|
43
44
|
"createdAt": "2026-09-16T13:45:23.000Z",
|
|
44
|
-
"updatedAt": "2026-09-
|
|
45
|
+
"updatedAt": "2026-09-17T14:14:56Z"
|
|
45
46
|
}
|
package/rcf/fbs/fbs-182.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"AC-15801-1",
|
|
12
12
|
"AC-15801-2",
|
|
13
13
|
"AC-15801-3",
|
|
14
|
+
"AC-15801-4",
|
|
14
15
|
"AC-15802-1",
|
|
15
16
|
"AC-15802-2"
|
|
16
17
|
],
|
|
@@ -38,5 +39,5 @@
|
|
|
38
39
|
"estimatedHours": 8,
|
|
39
40
|
"notes": "Extends the library manifest surface; downstream library owners can adopt at their own cadence.",
|
|
40
41
|
"createdAt": "2026-09-16T13:45:23.000Z",
|
|
41
|
-
"updatedAt": "2026-09-
|
|
42
|
+
"updatedAt": "2026-09-17T14:14:56Z"
|
|
42
43
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"Apply the eight redaction rules from design section 5 to title, body and evidence strings",
|
|
11
11
|
"Return a ledger of { rule, before, after, count } entries for the operator disclosure",
|
|
12
12
|
"Refuse on residual-secret matches (second pass) with a line pointer so the offending body is not sent",
|
|
13
|
-
"Ship a bundled allowlist of vendor hosts and honour the extension array on rcf/feedback-settings.json"
|
|
13
|
+
"Ship a bundled allowlist of vendor hosts and honour the extension array on rcf/feedback-settings.json",
|
|
14
|
+
"Guard operator-identity substitution with a RESERVED_IDENTITY_TOKENS stopword list (agent, user, admin, api, code, system, etc.) so a common noun in the operator's identity name never folds; substitution still fires for real given names in the identity"
|
|
14
15
|
],
|
|
15
16
|
"internalStructure": "src/feedback/redact.js is a pure module: redact(text, context) -> { text, ledger[] } plus small per-rule helpers. src/feedback/redact-allowlist.json ships the vendor host set; the extension array is merged at call time.",
|
|
16
17
|
"interfaces": [
|
|
@@ -35,5 +36,5 @@
|
|
|
35
36
|
"tradeoffs": "Rule-based redaction is testable with a fixture corpus and deterministic under review; the cost is that it will occasionally over-redact (a benign name that matches an entropy rule), which is what the disclosure ledger exists to catch.",
|
|
36
37
|
"notes": "The stored entry is never mutated by redaction; the rendered output is what leaves.",
|
|
37
38
|
"createdAt": "2026-09-16T13:45:23.000Z",
|
|
38
|
-
"updatedAt": "2026-09-
|
|
39
|
+
"updatedAt": "2026-09-17T13:47:09Z"
|
|
39
40
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"Walk entry.target -> manifest.blueprints[] -> registry entry -> issuesRepo, with sourceRef derivation as fallback",
|
|
11
11
|
"Return a { repo, visibility, derived, reason } shape; unresolved carries a reason for the doctor row",
|
|
12
12
|
"Route --kind core and shelf blueprints to the constant sourced from package.json:bugs.url (a test pins the constant)",
|
|
13
|
-
"Report the doctor feedback-destinations warn row for every registered library without a resolvable destination"
|
|
13
|
+
"Report the doctor feedback-destinations warn row for every registered library without a resolvable destination",
|
|
14
|
+
"Cross-check the registry BEFORE the shelf-constant fall-through for --kind blueprint entries with no libraryPrefix on the manifest record: one library owns the slug and resolves -> library destination (source: library-slug-inference); two-or-more libraries own the slug and resolve -> unresolved with reason ambiguous-library-slug and a candidates[] array; zero libraries own the slug -> shelf constant"
|
|
14
15
|
],
|
|
15
16
|
"internalStructure": "src/feedback/destination.js is a pure resolver; the constant lives at the top of the module and is exported for the test to compare against package.json.",
|
|
16
17
|
"interfaces": [
|
|
@@ -35,5 +36,5 @@
|
|
|
35
36
|
"tradeoffs": "A declared destination on library.json requires library owners to author one field; the cost is a one-time PR per library, in exchange for correctness on every consuming project.",
|
|
36
37
|
"notes": "A library mirrored from a fork would have derivation pointing at the fork; the declared issues field is why derivation is only a fallback.",
|
|
37
38
|
"createdAt": "2026-09-16T13:45:23.000Z",
|
|
38
|
-
"updatedAt": "2026-09-
|
|
39
|
+
"updatedAt": "2026-09-17T13:46:24.000Z"
|
|
39
40
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"createdAt": "2026-09-16T15:27:35Z",
|
|
3
|
-
"updatedAt": "2026-09-
|
|
3
|
+
"updatedAt": "2026-09-17T13:47:28Z",
|
|
4
4
|
"purpose": "Cover AC-15601-1 (rcf feedback preview renders each pending entry as the title and body that would be filed, lists the destination, and prints the redaction ledger over the full eight-rule vocabulary), AC-15601-2 (preview marks residual-secret findings so the operator sees before consenting; the submit half of AC-15601-2 lands with slice 4), AC-15601-3 (preview never mutates the stored entries.jsonl), AC-15601-4 (a final whole-text residual-secret pass on the fully rendered and normalised issue body and each bundle refuses submit on any hit) and AC-15701-1 (a stable 12-hex fingerprint is attached per entry, folds across blueprint versions when the anchor+class match, and falls back to normalised-title tokens with a warning when the anchor is absent).",
|
|
5
5
|
"testLevel": "integration",
|
|
6
6
|
"acIds": [
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"AC-15601-2",
|
|
9
9
|
"AC-15601-3",
|
|
10
10
|
"AC-15601-4",
|
|
11
|
-
"AC-15701-1"
|
|
11
|
+
"AC-15701-1",
|
|
12
|
+
"AC-15601-5"
|
|
12
13
|
],
|
|
13
14
|
"id": "TS-188",
|
|
14
15
|
"usId": "US-15601",
|
|
@@ -160,6 +161,13 @@
|
|
|
160
161
|
"description": "Cover AC-15601-1 (fix round 5, R4 negative case) via the unit test: ordinary prose that carries a vocabulary word (`the password field is required`, `the password is required`, `see commit abc1234 at HEAD`, `keyword=abcdef`, `authors=joe,jane`) does NOT fold because the value-shape gate rejects single-class values and values under 8 characters.",
|
|
161
162
|
"status": "pending",
|
|
162
163
|
"testPointer": "test/cli/feedback-fix-round-5.test.js::F-05 (round 5, R4 guard): a vocabulary word in ordinary prose (single-class value, or short value) does NOT fold"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "TC-188-redact-operator-identity-stopword-guard",
|
|
167
|
+
"acId": "AC-15601-5",
|
|
168
|
+
"description": "Cover AC-15601-5 via the unit test: redact runs over a body containing the docs URL 'https://stravica.ai/docs/rcf/how-the-agent-files-feedback', the prose 'when the agent stops the queue' and the personal name 'Alice reviewed this', with operatorName seeded as 'The Agent Alice'; the docs URL is emitted verbatim; the prose 'when the agent stops the queue' is emitted verbatim; 'Alice' folds to <operator>; no operator-identity ledger row names 'agent' or 'the'.",
|
|
169
|
+
"status": "pending",
|
|
170
|
+
"testPointer": "test/feedback/redact.test.js::AC-15601-5 operator-identity stopword guard: common nouns in the identity string are never folded; real given names still fold"
|
|
163
171
|
}
|
|
164
172
|
],
|
|
165
173
|
"status": "draft"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"createdAt": "2026-09-16T16:20:01Z",
|
|
3
|
-
"updatedAt": "2026-09-
|
|
3
|
+
"updatedAt": "2026-09-17T13:46:16Z",
|
|
4
4
|
"purpose": "Cover AC-15801-1 (library.json:issues object snapshotted onto rcf/blueprint-libraries.json:entries[] as issuesRepo and issuesVisibility on rcf library add; the review-on-add card prints the Feedback destination line naming repo and visibility), AC-15801-2 (destination.resolve walks entry -> manifest -> registry issuesRepo, falls back to sourceRef derivation for github URLs with derived:true, returns unresolved when neither is available; shelf and core route to Stravica/rcf-lite) and AC-15801-3 (rcf doctor --check feedback-destinations lists every registered library with no resolvable destination as a warn-only row with refusedByFix:true; exit stays 0).",
|
|
5
5
|
"testLevel": "integration",
|
|
6
6
|
"acIds": [
|
|
7
7
|
"AC-15801-1",
|
|
8
8
|
"AC-15801-2",
|
|
9
|
-
"AC-15801-3"
|
|
9
|
+
"AC-15801-3",
|
|
10
|
+
"AC-15801-4"
|
|
10
11
|
],
|
|
11
12
|
"id": "TS-189",
|
|
12
13
|
"usId": "US-15801",
|
|
@@ -32,6 +33,27 @@
|
|
|
32
33
|
"description": "Cover AC-15801-3 via the CLI test: rcf doctor --check feedback-destinations --json against a project with two registered libraries (one with issues, one without) returns exactly one drift row of item unresolved-library naming the second library, whose message includes the publisher.contact from the registry entry and whose refusedByFix flag is true; the overall doctor exit code stays 0 because the check is warn-only.",
|
|
33
34
|
"status": "pending",
|
|
34
35
|
"testPointer": "test/cli/feedback-destinations.test.js::AC-15801-3: rcf doctor --check feedback-destinations warns on a library without a resolvable destination and exits 0"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "TC-189-resolver-library-slug-inference-single",
|
|
39
|
+
"acId": "AC-15801-4",
|
|
40
|
+
"description": "Cover AC-15801-4 (single-library slug match) via the unit test: a bare-slug manifest record (no libraryPrefix) for 'application-spa' plus exactly one registered library ('wsd') that owns 'application-spa' and declares issuesRepo resolves to that library's declared destination with source 'library-slug-inference', derived false, kind library.",
|
|
41
|
+
"status": "pending",
|
|
42
|
+
"testPointer": "test/feedback/destination.test.js::AC-15801-4 library-slug-inference: bare-slug apply routes to the single library owning the slug when it declares issuesRepo"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "TC-189-resolver-library-slug-inference-ambiguous",
|
|
46
|
+
"acId": "AC-15801-4",
|
|
47
|
+
"description": "Cover AC-15801-4 (multi-library slug ambiguity) via the unit test: a bare-slug manifest record for 'application-spa' plus two registered libraries ('wsd' and 'ally') that both own 'application-spa' and both declare issuesRepo resolves to unresolved, reason 'ambiguous-library-slug', with candidates ['ally','wsd'] (sorted).",
|
|
48
|
+
"status": "pending",
|
|
49
|
+
"testPointer": "test/feedback/destination.test.js::AC-15801-4 library-slug-inference: two libraries owning the same slug return unresolved with reason ambiguous-library-slug"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "TC-189-resolver-library-slug-inference-none",
|
|
53
|
+
"acId": "AC-15801-4",
|
|
54
|
+
"description": "Cover AC-15801-4 (no-library fall-through) via the unit test: a bare-slug manifest record for 'security-auth-magic-link' plus registered libraries that do NOT own the slug falls through to the shelf constant unchanged from AC-15801-2.",
|
|
55
|
+
"status": "pending",
|
|
56
|
+
"testPointer": "test/feedback/destination.test.js::AC-15801-4 library-slug-inference: no library owns the slug -> shelf constant fall-through"
|
|
35
57
|
}
|
|
36
58
|
],
|
|
37
59
|
"status": "draft"
|
|
@@ -44,8 +44,17 @@
|
|
|
44
44
|
"then": "preview surfaces the residual line and the tainted entry stays pending; submit exits 3 without a create/comment for that entry and without writing a bundle whose text carries the residual shape",
|
|
45
45
|
"testable": true,
|
|
46
46
|
"scope": "runtime"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "AC-15601-5",
|
|
50
|
+
"description": "The operator-identity substitution (design section 5 rule 6) applies word-boundary and stopword guards so a common noun that happens to appear in the operator's identity name is not folded. A RESERVED_IDENTITY_TOKENS constant, exported from src/feedback/redact.js and defence-in-depth applied at every operator-name token loop, blocks the fold when the whitespace-separated token (case-insensitive) is one of: agent, agents, user, users, operator, operators, admin, admins, root, client, clients, server, servers, service, services, system, systems, name, main, test, code, api, apis, anon, anonymous, human, humans, dev, ops, sre, docs, developer, developers, engineer, engineers, team, teams. Substitution still fires for a real given name (Alice, Smith) even when the full identity string contains a stopword. The URL and prose canon phrase 'the agent files feedback' survives unmodified when the identity contains 'agent'. Rationale: the rcf-lite ecosystem uses 'agent' as a canonical noun (RULE 17, docs paths); operator-identity folds must be driven by real identity values, never by a bare common noun.",
|
|
51
|
+
"given": "a fixture project whose rcf/.identity/profile.md ## Name reads 'The Agent Alice', and a body containing the docs URL 'https://stravica.ai/docs/rcf/how-the-agent-files-feedback', the prose 'when the agent stops the queue', and the personal name 'Alice reviewed this'",
|
|
52
|
+
"when": "the redactor runs over the body with the seeded operator identity",
|
|
53
|
+
"then": "the docs URL survives verbatim; the prose 'when the agent stops the queue' survives verbatim; the personal name 'Alice' folds to <operator>; the ledger records an operator-identity row for 'Alice' but no row for 'agent' or 'the'",
|
|
54
|
+
"testable": true,
|
|
55
|
+
"scope": "runtime"
|
|
47
56
|
}
|
|
48
57
|
],
|
|
49
58
|
"createdAt": "2026-09-16T13:45:23.000Z",
|
|
50
|
-
"updatedAt": "2026-09-
|
|
59
|
+
"updatedAt": "2026-09-17T13:47:03Z"
|
|
51
60
|
}
|
|
@@ -35,8 +35,17 @@
|
|
|
35
35
|
"then": "doctor output carries one feedback-destinations warn row naming the second library, --fix is not offered on that row, exit code stays 0",
|
|
36
36
|
"testable": true,
|
|
37
37
|
"scope": "runtime"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "AC-15801-4",
|
|
41
|
+
"description": "The destination resolver cross-checks the registry BEFORE falling through to the shelf constant for `--kind blueprint` entries whose manifest record has no `libraryPrefix`. If exactly one registered library owns a blueprint with the applied slug AND that library resolves to a destination (issuesRepo, or a derivable git+https://github.com/... sourceRef), the resolver returns that library's destination with source: 'library-slug-inference' (derived: false for the declared case, derived: true for the sourceRef case). Two-or-more matches return unresolved with reason: 'ambiguous-library-slug' and a candidates[] array of library prefixes. Zero matches keep the current shelf-constant fall-through. This never overrides an existing libraryPrefix resolution: it only fires where the resolver would have picked the shelf constant.",
|
|
42
|
+
"given": "a fixture with three registered libraries and a bare-slug manifest record for 'application-spa'; case A: only library 'wsd' owns application-spa and declares issuesRepo 'wsd-team-dev/rcf-lite-blueprints'; case B: libraries 'wsd' and 'ally' both own application-spa and both declare issuesRepo; case C: no registered library owns application-spa",
|
|
43
|
+
"when": "destination.resolve() is called for each case",
|
|
44
|
+
"then": "case A resolves to 'wsd-team-dev/rcf-lite-blueprints' with source 'library-slug-inference' and derived false; case B resolves to unresolved with reason 'ambiguous-library-slug' and candidates ['ally','wsd']; case C resolves to Stravica/rcf-lite (shelf-constant fall-through), unchanged from AC-15801-2 shelf behaviour",
|
|
45
|
+
"testable": true,
|
|
46
|
+
"scope": "library"
|
|
38
47
|
}
|
|
39
48
|
],
|
|
40
49
|
"createdAt": "2026-09-16T13:45:23.000Z",
|
|
41
|
-
"updatedAt": "2026-09-
|
|
50
|
+
"updatedAt": "2026-09-17T13:46:02.000Z"
|
|
42
51
|
}
|
package/releases/releases.yaml
CHANGED
|
@@ -40,8 +40,18 @@
|
|
|
40
40
|
# `npm install rcf-lite`.
|
|
41
41
|
|
|
42
42
|
feedVersion: 1
|
|
43
|
-
latest: "0.28.
|
|
43
|
+
latest: "0.28.1"
|
|
44
44
|
releases:
|
|
45
|
+
- version: "0.28.1"
|
|
46
|
+
date: "2026-09-17"
|
|
47
|
+
breaking: false
|
|
48
|
+
headlines:
|
|
49
|
+
- "rcf-lite 0.28.1 fixes three defects the first WSD-lens round-trip surfaced on 0.28.0. Feedback on a library-owned blueprint now reaches the library owner even when the blueprint was applied by its bare slug, so a colleague running rcf define blueprint add application-spa against a project with a registered WSD library that also owns application-spa no longer sees the finding silently routed to Stravica/rcf-lite."
|
|
50
|
+
- "The operator-identity redactor no longer folds the canonical noun agent when it appears in an operator's ## Name line. The docs URL how-the-agent-files-feedback and prose like when the agent stops survive; real given names still fold. A common-noun stopword set (agent, user, admin, api, code, system, and the rest) is exported as RESERVED_IDENTITY_TOKENS."
|
|
51
|
+
- "The rcf feedback add help text now names --evidence as required and repeatable, matching the shipped behaviour and AC-15501-1. A README callout box discloses the rename from the retired @stravica-ai/rcf-build-lite name to rcf-lite so a colleague hitting no matching version on the old name has a paste-ready recovery command."
|
|
52
|
+
minAgentAction: null
|
|
53
|
+
notesUrl: "https://stravica.ai/docs/rcf/changelog/"
|
|
54
|
+
|
|
45
55
|
- version: "0.28.0"
|
|
46
56
|
date: "2026-09-17"
|
|
47
57
|
breaking: false
|
package/src/cli/feedback.js
CHANGED
|
@@ -136,8 +136,10 @@ Common options:
|
|
|
136
136
|
--body <text> | --body-file <path>
|
|
137
137
|
Body markdown, <= ${BODY_CAP_BYTES / 1024} KB
|
|
138
138
|
after UTF-8 encoding.
|
|
139
|
-
--evidence <pointer>
|
|
140
|
-
inside the project, or an id.
|
|
139
|
+
--evidence <pointer> Required, repeatable: a command, a
|
|
140
|
+
path:line inside the project, or an id.
|
|
141
|
+
At least one --evidence is required
|
|
142
|
+
(AC-15501-1); absence exits 2.
|
|
141
143
|
--harness claude-code|codex|other
|
|
142
144
|
Optional; inferred from env when absent.
|
|
143
145
|
--ask-now Blocker shortcut; flags the batch so the
|
|
@@ -954,6 +956,13 @@ async function handlePreview(argv, ctx) {
|
|
|
954
956
|
const reason = p.destination.reason ?? 'unresolved';
|
|
955
957
|
const contact = p.destination.publisherContact ? ` (library contact: ${p.destination.publisherContact})` : '';
|
|
956
958
|
stdout.write(` no issue destination declared${p.destination.reason ? ` (${reason})` : ''}; bundle will be written${contact}\n`);
|
|
959
|
+
// AC-15801-4 / design amendment R5b: for ambiguous-library-slug
|
|
960
|
+
// surface the candidate library prefixes so the operator sees
|
|
961
|
+
// exactly which qualified refs would resolve it.
|
|
962
|
+
if (Array.isArray(p.destination.candidates) && p.destination.candidates.length > 0) {
|
|
963
|
+
const qualified = p.destination.candidates.map((prefix) => `${prefix}:<slug>`).join(', ');
|
|
964
|
+
stdout.write(` candidate libraries owning this slug: ${p.destination.candidates.join(', ')} (re-apply with a qualified ref, e.g. ${qualified})\n`);
|
|
965
|
+
}
|
|
957
966
|
}
|
|
958
967
|
stdout.write(`fingerprint: ${p.fingerprint}\n`);
|
|
959
968
|
if (p.fingerprintFallback) {
|
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Resolution order (design section 3.3):
|
|
9
9
|
// 1. --kind core -> CORE_REPO (public)
|
|
10
|
-
// 2. --kind blueprint, no libraryPrefix ->
|
|
10
|
+
// 2. --kind blueprint, no libraryPrefix -> library-slug inference
|
|
11
|
+
// (AC-15801-4 / design amendment R5a): if the applied slug is
|
|
12
|
+
// owned by exactly one registered library that resolves to a
|
|
13
|
+
// destination (issuesRepo or derivable sourceRef), route there
|
|
14
|
+
// with source: 'library-slug-inference'; if two or more libraries
|
|
15
|
+
// own the slug and resolve, return unresolved with reason
|
|
16
|
+
// 'ambiguous-library-slug' and a candidates[] array of prefixes;
|
|
17
|
+
// otherwise fall through to CORE_REPO (shelf; public).
|
|
11
18
|
// 3. --kind blueprint, libraryPrefix -> registry entry issuesRepo
|
|
12
19
|
// (declared destination wins;
|
|
13
20
|
// visibility from the entry)
|
|
@@ -210,9 +217,10 @@ function recordEffectiveSlug(record) {
|
|
|
210
217
|
* @property {'public' | 'private' | 'unresolved'} visibility
|
|
211
218
|
* @property {'core' | 'shelf' | 'library'} kind
|
|
212
219
|
* @property {boolean} derived true when repo came from sourceRef derivation
|
|
213
|
-
* @property {string} [reason] filled on unresolved: 'no-issues-field' | 'no-github-source' | 'no-libraryprefix-record' | 'unknown-target'
|
|
220
|
+
* @property {string} [reason] filled on unresolved: 'no-issues-field' | 'no-github-source' | 'no-libraryprefix-record' | 'unknown-target' | 'ambiguous-library-slug'
|
|
221
|
+
* @property {string[]} [candidates] filled on unresolved with reason 'ambiguous-library-slug': sorted library prefixes owning the slug
|
|
214
222
|
* @property {string} [publisherContact] registry entry's publisher.contact (bundle uses it)
|
|
215
|
-
* @property {string} [source] 'library-manifest' | 'sourceRef-derivation' | 'package-bugs-url' | 'shelf-constant'
|
|
223
|
+
* @property {string} [source] 'library-manifest' | 'sourceRef-derivation' | 'package-bugs-url' | 'shelf-constant' | 'library-slug-inference'
|
|
216
224
|
*/
|
|
217
225
|
|
|
218
226
|
/**
|
|
@@ -263,8 +271,27 @@ export async function resolve(entry, context = {}) {
|
|
|
263
271
|
|
|
264
272
|
const libraryPrefix = record.libraryPrefix ?? entry?.target?.libraryPrefix ?? null;
|
|
265
273
|
|
|
266
|
-
// Shelf blueprint (no libraryPrefix
|
|
274
|
+
// Shelf blueprint (no libraryPrefix on the manifest record). Before
|
|
275
|
+
// falling through to the shelf constant, cross-check the registry:
|
|
276
|
+
// if exactly one registered library owns a blueprint with the applied
|
|
277
|
+
// slug AND that library resolves to a destination, route to that
|
|
278
|
+
// library so a bare-slug apply of a library-shared name still lands
|
|
279
|
+
// on the library owner's repo (design amendment R5, AC-15801-4).
|
|
267
280
|
if (!libraryPrefix) {
|
|
281
|
+
const inferred = inferLibraryFromSlug(record, registry);
|
|
282
|
+
if (inferred.kind === 'single') {
|
|
283
|
+
return inferred.destination;
|
|
284
|
+
}
|
|
285
|
+
if (inferred.kind === 'ambiguous') {
|
|
286
|
+
return {
|
|
287
|
+
repo: null,
|
|
288
|
+
visibility: 'unresolved',
|
|
289
|
+
kind: 'library',
|
|
290
|
+
derived: false,
|
|
291
|
+
reason: 'ambiguous-library-slug',
|
|
292
|
+
candidates: inferred.candidates,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
268
295
|
return {
|
|
269
296
|
repo: CORE_REPO,
|
|
270
297
|
visibility: 'public',
|
|
@@ -359,3 +386,70 @@ function findLibraryEntry(registry, libraryPrefix) {
|
|
|
359
386
|
const libs = Array.isArray(registry?.libraries) ? registry.libraries : [];
|
|
360
387
|
return libs.find((l) => l?.libraryPrefix === libraryPrefix) ?? null;
|
|
361
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* AC-15801-4 / design amendment R5a: given a manifest record with no
|
|
392
|
+
* libraryPrefix, cross-check the registry to see whether the applied
|
|
393
|
+
* slug is owned by exactly one registered library that resolves to a
|
|
394
|
+
* destination. Returns:
|
|
395
|
+
* { kind: 'none' } no library owns the slug (or none resolve)
|
|
396
|
+
* { kind: 'single', destination } exactly one library owns the slug and resolves
|
|
397
|
+
* { kind: 'ambiguous', candidates: string[] } two or more libraries own the slug and resolve
|
|
398
|
+
*
|
|
399
|
+
* The resolver only calls this on the shelf-constant fall-through
|
|
400
|
+
* branch, so it never overrides an existing libraryPrefix resolution.
|
|
401
|
+
*
|
|
402
|
+
* @param {object} record manifest.blueprints[] record
|
|
403
|
+
* @param {object | null | undefined} registry parsed rcf/blueprint-libraries.json
|
|
404
|
+
*/
|
|
405
|
+
function inferLibraryFromSlug(record, registry) {
|
|
406
|
+
const slug = typeof record?.slug === 'string' ? record.slug : null;
|
|
407
|
+
if (!slug) return { kind: 'none' };
|
|
408
|
+
const libs = Array.isArray(registry?.libraries) ? registry.libraries : [];
|
|
409
|
+
const owners = [];
|
|
410
|
+
for (const lib of libs) {
|
|
411
|
+
if (!lib || typeof lib !== 'object') continue;
|
|
412
|
+
if (typeof lib.libraryPrefix !== 'string' || lib.libraryPrefix.length === 0) continue;
|
|
413
|
+
const bps = Array.isArray(lib.blueprints) ? lib.blueprints : [];
|
|
414
|
+
const owns = bps.some((b) => b && typeof b.slug === 'string' && b.slug === slug);
|
|
415
|
+
if (!owns) continue;
|
|
416
|
+
const declared = typeof lib.issuesRepo === 'string' && lib.issuesRepo.length > 0 ? lib.issuesRepo : null;
|
|
417
|
+
const derivedRepo = declared ? null : parseGithubSource(lib.sourceRef ?? '');
|
|
418
|
+
if (!declared && !derivedRepo) continue;
|
|
419
|
+
const contact = typeof lib?.publisher?.contact === 'string' ? lib.publisher.contact : undefined;
|
|
420
|
+
owners.push({ lib, declared, derivedRepo, contact });
|
|
421
|
+
}
|
|
422
|
+
if (owners.length === 0) return { kind: 'none' };
|
|
423
|
+
if (owners.length === 1) {
|
|
424
|
+
const [o] = owners;
|
|
425
|
+
if (o.declared) {
|
|
426
|
+
const declaredVisibility = o.lib.issuesVisibility === 'private' ? 'private'
|
|
427
|
+
: o.lib.issuesVisibility === 'public' ? 'public'
|
|
428
|
+
: 'public';
|
|
429
|
+
return {
|
|
430
|
+
kind: 'single',
|
|
431
|
+
destination: {
|
|
432
|
+
repo: o.declared,
|
|
433
|
+
visibility: declaredVisibility,
|
|
434
|
+
kind: 'library',
|
|
435
|
+
derived: false,
|
|
436
|
+
source: 'library-slug-inference',
|
|
437
|
+
...(o.contact ? { publisherContact: o.contact } : {}),
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
kind: 'single',
|
|
443
|
+
destination: {
|
|
444
|
+
repo: o.derivedRepo,
|
|
445
|
+
visibility: 'public',
|
|
446
|
+
kind: 'library',
|
|
447
|
+
derived: true,
|
|
448
|
+
source: 'library-slug-inference',
|
|
449
|
+
...(o.contact ? { publisherContact: o.contact } : {}),
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const candidates = owners.map((o) => o.lib.libraryPrefix).sort();
|
|
454
|
+
return { kind: 'ambiguous', candidates };
|
|
455
|
+
}
|
package/src/feedback/redact.js
CHANGED
|
@@ -32,6 +32,26 @@ const BUNDLED_ALLOWLIST = JSON.parse(
|
|
|
32
32
|
/** Body size cap (bytes). Mirrors src/cli/feedback.js:BODY_CAP_BYTES. */
|
|
33
33
|
export const BODY_CAP_BYTES = 8 * 1024;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Common-noun stopwords the operator-identity substitution never folds
|
|
37
|
+
* (AC-15601-5 / design amendment R5c). The rcf-lite ecosystem uses
|
|
38
|
+
* `agent` as a canonical noun (RULE 17, docs paths such as
|
|
39
|
+
* `how-the-agent-files-feedback`); if the operator's identity name
|
|
40
|
+
* happens to contain one of these words, the token loop skips it so
|
|
41
|
+
* the docs URL and prose like `when the agent stops` survive. Real
|
|
42
|
+
* given names in the identity still fold. The list is intentionally
|
|
43
|
+
* conservative: anything a normal profile.md ## Name line might carry
|
|
44
|
+
* as a common English or engineering noun.
|
|
45
|
+
*/
|
|
46
|
+
export const RESERVED_IDENTITY_TOKENS = Object.freeze(new Set([
|
|
47
|
+
'agent', 'agents', 'user', 'users', 'operator', 'operators',
|
|
48
|
+
'admin', 'admins', 'root', 'client', 'clients', 'server', 'servers',
|
|
49
|
+
'service', 'services', 'system', 'systems', 'name', 'main', 'test',
|
|
50
|
+
'code', 'api', 'apis', 'anon', 'anonymous', 'human', 'humans',
|
|
51
|
+
'dev', 'ops', 'sre', 'docs', 'developer', 'developers', 'engineer',
|
|
52
|
+
'engineers', 'team', 'teams',
|
|
53
|
+
]));
|
|
54
|
+
|
|
35
55
|
/**
|
|
36
56
|
* Open secret-key vocabulary (design amendment R3a). A key/value pair
|
|
37
57
|
* is treated as a secret when the key, right-bounded at a word edge so
|
|
@@ -389,9 +409,20 @@ export function redact(input, context = {}) {
|
|
|
389
409
|
if (context.operatorName) {
|
|
390
410
|
const opName = context.operatorName.trim();
|
|
391
411
|
if (opName.length > 0) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
412
|
+
// AC-15601-5 / design amendment R5c: never fold a common noun
|
|
413
|
+
// that happens to appear in the identity string. The rcf-lite
|
|
414
|
+
// ecosystem uses 'agent' as a canonical noun (RULE 17, docs
|
|
415
|
+
// paths); folding it as operator-identity mangles the docs URL
|
|
416
|
+
// `how-the-agent-files-feedback` and prose like `when the
|
|
417
|
+
// agent stops`. Substitution still fires for a real given name
|
|
418
|
+
// (Alice, Smith) even when the full identity string carries a
|
|
419
|
+
// stopword. When the identity contains ONLY stopwords (a
|
|
420
|
+
// placeholder-in-disguise), skip identity folds entirely.
|
|
421
|
+
const rawTokens = opName.split(/\s+/);
|
|
422
|
+
const foldable = rawTokens.filter((t) => t.length > 3 && !RESERVED_IDENTITY_TOKENS.has(t.toLowerCase()));
|
|
423
|
+
if (foldable.length > 0) {
|
|
424
|
+
text = replaceAndLog(text, escapedLiteral(opName), '<operator>', 'operator-identity', ledger);
|
|
425
|
+
for (const tok of foldable) {
|
|
395
426
|
text = replaceAndLog(text, `\\b${escapeRe(tok)}\\b`, '<operator>', 'operator-identity', ledger);
|
|
396
427
|
}
|
|
397
428
|
}
|
|
@@ -399,7 +430,7 @@ export function redact(input, context = {}) {
|
|
|
399
430
|
}
|
|
400
431
|
if (context.projectName) {
|
|
401
432
|
const pn = context.projectName.trim();
|
|
402
|
-
if (pn.length > 0) {
|
|
433
|
+
if (pn.length > 0 && !RESERVED_IDENTITY_TOKENS.has(pn.toLowerCase())) {
|
|
403
434
|
text = replaceAndLog(text, `\\b${escapeRe(pn)}\\b`, '<project>', 'operator-identity', ledger);
|
|
404
435
|
}
|
|
405
436
|
}
|
|
@@ -846,6 +877,15 @@ function looksHighEntropyPath(value) {
|
|
|
846
877
|
if (typeof value !== 'string') return false;
|
|
847
878
|
if (value.length < 16) return false;
|
|
848
879
|
if (!/^[A-Za-z0-9._~+/=-]+$/.test(value)) return false;
|
|
880
|
+
// AC-15601-5 canon-phrase guard: a lowercase kebab-case segment with
|
|
881
|
+
// three or more hyphen-separated word parts is a documentation slug,
|
|
882
|
+
// not a credential. The rcf-lite docs URL
|
|
883
|
+
// `how-the-agent-files-feedback` is 28 chars, 2 classes (lowercase
|
|
884
|
+
// and separator), and would otherwise trip the 16-char, 2-class rule
|
|
885
|
+
// and produce `<url-credential>` inside the docs URL. Real credential
|
|
886
|
+
// segments in URL paths (webhook tokens, session ids, presigned
|
|
887
|
+
// credentials) are mixed-case-alphanumeric, not kebab-case English.
|
|
888
|
+
if (/^[a-z]+(-[a-z]+){2,}$/.test(value)) return false;
|
|
849
889
|
let classes = 0;
|
|
850
890
|
if (/[A-Z]/.test(value)) classes += 1;
|
|
851
891
|
if (/[a-z]/.test(value)) classes += 1;
|