cleargate 0.14.0 → 0.15.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 +21 -0
- package/dist/MANIFEST.json +72 -16
- package/dist/admin-api/index.cjs +0 -1
- package/dist/admin-api/index.js +1 -2
- package/dist/auth/factory.cjs +0 -1
- package/dist/auth/factory.js +2 -3
- package/dist/auth/require-token.cjs +0 -1
- package/dist/auth/require-token.js +1 -2
- package/dist/auth/token-store.cjs +0 -1
- package/dist/auth/token-store.js +1 -2
- package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
- package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
- package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
- package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
- package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
- package/dist/cli.cjs +1564 -1414
- package/dist/cli.js +1514 -1364
- package/dist/lib/ledger.cjs +0 -1
- package/dist/lib/ledger.js +1 -2
- package/dist/lib/lifecycle-reconcile.cjs +0 -1
- package/dist/lib/lifecycle-reconcile.js +2 -3
- package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
- package/package.json +4 -3
- package/templates/cleargate-planning/.claude/agents/architect-synth.md +2 -0
- package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
- package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
- package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
- package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
- package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
- package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +222 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
- package/templates/cleargate-planning/MANIFEST.json +72 -16
- package/dist/admin-api/index.cjs.map +0 -1
- package/dist/admin-api/index.js.map +0 -1
- package/dist/auth/factory.cjs.map +0 -1
- package/dist/auth/factory.js.map +0 -1
- package/dist/auth/require-token.cjs.map +0 -1
- package/dist/auth/require-token.js.map +0 -1
- package/dist/auth/token-store.cjs.map +0 -1
- package/dist/auth/token-store.js.map +0 -1
- package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
- package/dist/chunk-5DI2Z3C2.js.map +0 -1
- package/dist/chunk-BTSZOEWC.js.map +0 -1
- package/dist/chunk-E3X7IE5E.js.map +0 -1
- package/dist/chunk-PDE37WFQ.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/lib/ledger.cjs.map +0 -1
- package/dist/lib/ledger.js.map +0 -1
- package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
- package/dist/lib/lifecycle-reconcile.js.map +0 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
- package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
- package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
- package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
- package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
- package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
- package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
- package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
- package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
- package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
- package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
- package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
- package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
- package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
- package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
- package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
- package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
- package/dist/templates/synthesis/active-sprint.md +0 -30
- package/dist/templates/synthesis/open-gates.md +0 -38
- package/dist/templates/synthesis/product-state.md +0 -31
- package/dist/templates/synthesis/roadmap.md +0 -63
- package/dist/whoami-EANGN46Z.js.map +0 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* qa_red_lint.mjs — Semantic QA-Red fixture lint (CR-081).
|
|
4
|
+
*
|
|
5
|
+
* Usage: node .cleargate/scripts/qa_red_lint.mjs <dir>
|
|
6
|
+
*
|
|
7
|
+
* Globs QA-Red test files under <dir> by their red-test naming convention:
|
|
8
|
+
* *.red.node.test.ts (meta-repo default)
|
|
9
|
+
* *.red.test.ts
|
|
10
|
+
* *.red.test.tsx
|
|
11
|
+
* test_*_red.py
|
|
12
|
+
*
|
|
13
|
+
* Scope: ONLY actual test files — NOT *.red.sh bash harnesses (prevents
|
|
14
|
+
* self-flagging the CR-081 harness which contains HEREDOC fixture strings).
|
|
15
|
+
*
|
|
16
|
+
* Exit 0 = clean (no flags).
|
|
17
|
+
* Exit 1 = ≥1 rule flagged (messages written to stderr: file:line rule-id fix).
|
|
18
|
+
*
|
|
19
|
+
* Rule registry (structured for growth — R-enum + R-query only in first ship):
|
|
20
|
+
*
|
|
21
|
+
* R-enum: Detects when a fixture constructs a typed model with an enum/Literal
|
|
22
|
+
* field whose argument is a string literal NOT in the statically-declared set.
|
|
23
|
+
* Conservative: only flags when BOTH the declared set AND the out-of-set
|
|
24
|
+
* literal are statically resolvable in the same file. Never flags when the set
|
|
25
|
+
* cannot be resolved.
|
|
26
|
+
*
|
|
27
|
+
* R-query: Flags queryByText(<s>) / getByText(<s>) whose target string <s> is
|
|
28
|
+
* duplicated across ≥2 rows in the same render-input literal within the test
|
|
29
|
+
* body. Recommends queryAllByText(<s>)[0] / getByTestId(...).
|
|
30
|
+
*
|
|
31
|
+
* No-false-flag guarantee: a file with no out-of-set Literal AND no
|
|
32
|
+
* duplicate-string getByText/queryByText → zero flags → exit 0.
|
|
33
|
+
* This specifically ensures CR-082's plain node:test file exits 0.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Rule registry — {id, description, scan(fileText) -> [{line, message}]}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* R-enum: Detect out-of-set literal in a Pydantic-style or TS-union typed constructor.
|
|
45
|
+
*
|
|
46
|
+
* Pattern:
|
|
47
|
+
* 1. Find Literal[...] declarations — e.g. Literal["a", "b", "c"]
|
|
48
|
+
* 2. Find TS string-union type aliases — e.g. type Theme = "a" | "b" | "c"
|
|
49
|
+
* 3. Find TS enums — e.g. enum Theme { A = "a", B = "b" }
|
|
50
|
+
* 4. Find constructor/prop calls that pass a bare string literal for a field
|
|
51
|
+
* whose allowed set was found in step 1-3.
|
|
52
|
+
* 5. Flag if the string literal is NOT in the allowed set.
|
|
53
|
+
*
|
|
54
|
+
* Conservative: only flags when set resolution succeeded AND the literal is
|
|
55
|
+
* statically visible (a bare string, not a variable/expression).
|
|
56
|
+
*/
|
|
57
|
+
const rEnum = {
|
|
58
|
+
id: 'R-enum',
|
|
59
|
+
description: 'Constructed-fixture enum/Literal validity — literal argument not in declared set',
|
|
60
|
+
scan(fileText) {
|
|
61
|
+
const flags = [];
|
|
62
|
+
const lines = fileText.split('\n');
|
|
63
|
+
|
|
64
|
+
// ---- Step 1: Extract Pydantic Literal[...] sets ----
|
|
65
|
+
// Pattern: Literal["a", "b", "c"] or Literal['a', 'b', 'c']
|
|
66
|
+
// We collect all such sets from the file and build a map of set_values → declared.
|
|
67
|
+
// We then check constructor kwarg calls where theme=<literal>.
|
|
68
|
+
//
|
|
69
|
+
// Strategy: scan the file for lines containing Literal[...] and extract members.
|
|
70
|
+
// Then scan for constructor kwargs of the form field="value" or field='value'
|
|
71
|
+
// where the field name matches a Literal-typed field seen earlier.
|
|
72
|
+
|
|
73
|
+
/** @type {Map<string, {values: Set<string>, fieldName: string}>} */
|
|
74
|
+
const literalSets = new Map(); // fieldName -> {values}
|
|
75
|
+
|
|
76
|
+
// Regex: <fieldName>: Literal["v1", "v2", ...]
|
|
77
|
+
// Handles multi-line with a single-line scan (most fixture files are compact)
|
|
78
|
+
const literalTypeRegex = /(\w+)\s*:\s*Literal\[([^\]]+)\]/g;
|
|
79
|
+
for (const match of fileText.matchAll(literalTypeRegex)) {
|
|
80
|
+
const fieldName = match[1];
|
|
81
|
+
const rawValues = match[2];
|
|
82
|
+
const values = new Set();
|
|
83
|
+
// Extract quoted strings from the Literal[...] content
|
|
84
|
+
for (const vm of rawValues.matchAll(/["']([^"']+)["']/g)) {
|
|
85
|
+
values.add(vm[1]);
|
|
86
|
+
}
|
|
87
|
+
if (values.size > 0) {
|
|
88
|
+
literalSets.set(fieldName, values);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- Step 2: Extract TS string-union type aliases ----
|
|
93
|
+
// Pattern: type <Name> = "a" | "b" | "c"
|
|
94
|
+
// We map the type NAME to its value set.
|
|
95
|
+
/** @type {Map<string, Set<string>>} */
|
|
96
|
+
const tsUnionTypes = new Map(); // typeName -> values
|
|
97
|
+
|
|
98
|
+
const tsUnionRegex = /type\s+(\w+)\s*=\s*((?:"[^"]*"|'[^']*')\s*(?:\|\s*(?:"[^"]*"|'[^']*')\s*)*)/g;
|
|
99
|
+
for (const match of fileText.matchAll(tsUnionRegex)) {
|
|
100
|
+
const typeName = match[1];
|
|
101
|
+
const rawUnion = match[2];
|
|
102
|
+
const values = new Set();
|
|
103
|
+
for (const vm of rawUnion.matchAll(/["']([^"']+)["']/g)) {
|
|
104
|
+
values.add(vm[1]);
|
|
105
|
+
}
|
|
106
|
+
if (values.size > 0) {
|
|
107
|
+
tsUnionTypes.set(typeName, values);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- Step 3: Extract TS enums ----
|
|
112
|
+
// Pattern: enum <Name> { A = "a", B = "b" }
|
|
113
|
+
/** @type {Map<string, Set<string>>} */
|
|
114
|
+
const tsEnums = new Map(); // enumName -> values
|
|
115
|
+
|
|
116
|
+
const tsEnumRegex = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
117
|
+
for (const match of fileText.matchAll(tsEnumRegex)) {
|
|
118
|
+
const enumName = match[1];
|
|
119
|
+
const body = match[2];
|
|
120
|
+
const values = new Set();
|
|
121
|
+
for (const vm of body.matchAll(/=\s*["']([^"']+)["']/g)) {
|
|
122
|
+
values.add(vm[1]);
|
|
123
|
+
}
|
|
124
|
+
if (values.size > 0) {
|
|
125
|
+
tsEnums.set(enumName, values);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Merge all TS typed sets into a field-name map.
|
|
130
|
+
// For TS: we also look for prop declarations like: fieldName: TypeName
|
|
131
|
+
// and then check constructor calls with fieldName: "value"
|
|
132
|
+
/** @type {Map<string, Set<string>>} */
|
|
133
|
+
const allFieldSets = new Map(literalSets); // start with Python Literal sets
|
|
134
|
+
|
|
135
|
+
// For TS unions and enums, find property declarations that use them:
|
|
136
|
+
// pattern: <fieldName>: <TypeName> (in interfaces or class definitions)
|
|
137
|
+
const tsPropDeclRegex = /(\w+)\s*:\s*(\w+)/g;
|
|
138
|
+
for (const match of fileText.matchAll(tsPropDeclRegex)) {
|
|
139
|
+
const propName = match[1];
|
|
140
|
+
const typeName = match[2];
|
|
141
|
+
const values = tsUnionTypes.get(typeName) ?? tsEnums.get(typeName);
|
|
142
|
+
if (values && !allFieldSets.has(propName)) {
|
|
143
|
+
allFieldSets.set(propName, values);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (allFieldSets.size === 0) {
|
|
148
|
+
// No declared sets found — nothing to check
|
|
149
|
+
return flags;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- Step 4: Check constructor/object calls for out-of-set literals ----
|
|
153
|
+
// Python: FieldName(field="value") or FieldName(field='value')
|
|
154
|
+
// TS: { field: "value" } or field="value" in JSX/React props
|
|
155
|
+
|
|
156
|
+
for (const [fieldName, allowedValues] of allFieldSets) {
|
|
157
|
+
// Scan every line with the given regex (capture group 1 = the literal),
|
|
158
|
+
// flagging any literal not in allowedValues. Shared by the Python-kwarg
|
|
159
|
+
// and TS-object-prop passes below (identical flag logic, different regex).
|
|
160
|
+
const scanForOutOfSet = (regex) => {
|
|
161
|
+
let lineIndex = 0;
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
lineIndex++;
|
|
164
|
+
for (const match of line.matchAll(regex)) {
|
|
165
|
+
const literal = match[1];
|
|
166
|
+
if (!allowedValues.has(literal)) {
|
|
167
|
+
flags.push({
|
|
168
|
+
line: lineIndex,
|
|
169
|
+
message: `R-enum: field "${fieldName}" value "${literal}" is not in the declared set [${[...allowedValues].map(v => `"${v}"`).join(', ')}]. Fix: use one of the declared values.`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Python kwarg: fieldName="value" or fieldName='value'
|
|
177
|
+
const pythonKwargRegex = new RegExp(
|
|
178
|
+
`\\b${fieldName}\\s*=\\s*["']([^"']+)["']`,
|
|
179
|
+
'g'
|
|
180
|
+
);
|
|
181
|
+
scanForOutOfSet(pythonKwargRegex);
|
|
182
|
+
|
|
183
|
+
// TS object prop: fieldName: "value" — only when it looks like a value assignment (in { } context)
|
|
184
|
+
// We look for patterns like: fieldName: "value" but NOT fieldName: TypeName (declaration)
|
|
185
|
+
// Heuristic: in the same line, if it's preceded by whitespace or comma+whitespace and followed
|
|
186
|
+
// by a string literal (not a type identifier like a capital-letter word), flag it.
|
|
187
|
+
const tsObjPropRegex = new RegExp(
|
|
188
|
+
`(?:,|\\{|^)\\s*${fieldName}\\s*:\\s*["']([^"']+)["']`,
|
|
189
|
+
'g'
|
|
190
|
+
);
|
|
191
|
+
scanForOutOfSet(tsObjPropRegex);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return flags;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* R-query: Detect queryByText(<s>) / getByText(<s>) where <s> appears on
|
|
200
|
+
* ≥2 rows in the same render-input object literal within the test body.
|
|
201
|
+
*
|
|
202
|
+
* Pattern:
|
|
203
|
+
* 1. Find array literals used as render input (rows/data/items arrays)
|
|
204
|
+
* that contain objects with string values.
|
|
205
|
+
* 2. Collect all string values that appear ≥2 times across the array items.
|
|
206
|
+
* 3. For each queryByText/getByText call whose argument matches a duplicated string,
|
|
207
|
+
* flag it with a recommendation.
|
|
208
|
+
*
|
|
209
|
+
* Conservative: only flags when:
|
|
210
|
+
* - The duplicated string is a plain string literal (not variable).
|
|
211
|
+
* - The queryByText/getByText call uses the exact same plain string literal.
|
|
212
|
+
*/
|
|
213
|
+
const rQuery = {
|
|
214
|
+
id: 'R-query',
|
|
215
|
+
description: 'query-by-text single-match hazard — target text is duplicated in render input',
|
|
216
|
+
scan(fileText) {
|
|
217
|
+
const flags = [];
|
|
218
|
+
const lines = fileText.split('\n');
|
|
219
|
+
|
|
220
|
+
// ---- Step 1: Collect all string literals that appear ≥2 times in object
|
|
221
|
+
// property values within array literals (render-input rows). ----
|
|
222
|
+
//
|
|
223
|
+
// Strategy: scan the file for string literal values in object contexts.
|
|
224
|
+
// Count occurrences of each string value in what looks like array-of-objects.
|
|
225
|
+
//
|
|
226
|
+
// We use a conservative heuristic: look for patterns like:
|
|
227
|
+
// detail: 'Connected' or detail: "Connected"
|
|
228
|
+
// status: 'ok'
|
|
229
|
+
// within array contexts (we check the whole file for simplicity,
|
|
230
|
+
// counting how many times each value appears in property contexts).
|
|
231
|
+
//
|
|
232
|
+
// Strip line comments (//) before scanning to avoid counting comment text.
|
|
233
|
+
|
|
234
|
+
/** @type {Map<string, number>} */
|
|
235
|
+
const propValueCounts = new Map();
|
|
236
|
+
|
|
237
|
+
// Strip single-line (//) comments before scanning for property values.
|
|
238
|
+
// This prevents comment text from inflating property value counts.
|
|
239
|
+
const strippedLines = lines.map(line => {
|
|
240
|
+
// Remove // comment suffix — naive but sufficient for fixture files.
|
|
241
|
+
// Does not handle strings containing // (e.g. URLs in strings) but
|
|
242
|
+
// fixture files in QA-Red tests rarely contain such patterns.
|
|
243
|
+
const commentIdx = line.indexOf('//');
|
|
244
|
+
return commentIdx >= 0 ? line.slice(0, commentIdx) : line;
|
|
245
|
+
});
|
|
246
|
+
const strippedText = strippedLines.join('\n');
|
|
247
|
+
|
|
248
|
+
// Match object property assignments: key: "value" or key: 'value'
|
|
249
|
+
// We want the VALUE part (the string literal)
|
|
250
|
+
// Only scan non-comment portions of the file.
|
|
251
|
+
const objPropValueRegex = /:\s*["']([^"']+)["']/g;
|
|
252
|
+
for (const match of strippedText.matchAll(objPropValueRegex)) {
|
|
253
|
+
const val = match[1];
|
|
254
|
+
propValueCounts.set(val, (propValueCounts.get(val) ?? 0) + 1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Find strings that appear ≥2 times as property values
|
|
258
|
+
const duplicatedValues = new Set(
|
|
259
|
+
[...propValueCounts.entries()]
|
|
260
|
+
.filter(([, count]) => count >= 2)
|
|
261
|
+
.map(([val]) => val)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (duplicatedValues.size === 0) {
|
|
265
|
+
return flags;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---- Step 2: Find queryByText / getByText calls that use duplicated strings ----
|
|
269
|
+
const queryRegex = /(?:queryByText|getByText)\(\s*["']([^"']+)["']\s*\)/g;
|
|
270
|
+
let lineIndex = 0;
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
lineIndex++;
|
|
273
|
+
for (const match of line.matchAll(queryRegex)) {
|
|
274
|
+
const queryStr = match[1];
|
|
275
|
+
if (duplicatedValues.has(queryStr)) {
|
|
276
|
+
const fnName = match[0].startsWith('queryByText') ? 'queryByText' : 'getByText';
|
|
277
|
+
flags.push({
|
|
278
|
+
line: lineIndex,
|
|
279
|
+
message: `R-query: ${fnName}('${queryStr}') targets a string that appears on ≥2 rows in the render input — this will throw "Found multiple elements". Fix: use queryAllByText('${queryStr}')[0] or getByTestId(...) instead.`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return flags;
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Rule registry (ordered; add new rules by appending)
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
const RULES = [rEnum, rQuery];
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// File glob — scan only red test files, NOT bash harnesses
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
const RED_TEST_EXTENSIONS = [
|
|
298
|
+
/\.red\.node\.test\.ts$/,
|
|
299
|
+
/\.red\.test\.ts$/,
|
|
300
|
+
/\.red\.test\.tsx$/,
|
|
301
|
+
/^test_.*_red\.py$/,
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Recursively collect all red-test files under a directory.
|
|
306
|
+
* Excludes node_modules, .git, and *.red.sh files (bash harnesses).
|
|
307
|
+
* @param {string} dir
|
|
308
|
+
* @returns {string[]} absolute paths
|
|
309
|
+
*/
|
|
310
|
+
function collectRedTestFiles(dir) {
|
|
311
|
+
const results = [];
|
|
312
|
+
let entries;
|
|
313
|
+
try {
|
|
314
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
315
|
+
} catch {
|
|
316
|
+
return results;
|
|
317
|
+
}
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
const fullPath = path.join(dir, entry.name);
|
|
320
|
+
if (entry.isDirectory()) {
|
|
321
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
322
|
+
results.push(...collectRedTestFiles(fullPath));
|
|
323
|
+
} else if (entry.isFile()) {
|
|
324
|
+
const name = entry.name;
|
|
325
|
+
// Must match a red-test pattern AND must NOT be a .sh file
|
|
326
|
+
if (name.endsWith('.sh')) continue;
|
|
327
|
+
if (RED_TEST_EXTENSIONS.some(re => re.test(name))) {
|
|
328
|
+
results.push(fullPath);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Main
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
const [, , dirArg] = process.argv;
|
|
339
|
+
|
|
340
|
+
if (!dirArg) {
|
|
341
|
+
process.stderr.write('Usage: node qa_red_lint.mjs <dir>\n');
|
|
342
|
+
process.exit(2);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const targetDir = path.resolve(dirArg);
|
|
346
|
+
|
|
347
|
+
if (!fs.existsSync(targetDir)) {
|
|
348
|
+
process.stderr.write(`qa_red_lint: directory does not exist: ${targetDir}\n`);
|
|
349
|
+
process.exit(2);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const testFiles = collectRedTestFiles(targetDir);
|
|
353
|
+
|
|
354
|
+
let totalFlags = 0;
|
|
355
|
+
|
|
356
|
+
for (const filePath of testFiles) {
|
|
357
|
+
let fileText;
|
|
358
|
+
try {
|
|
359
|
+
fileText = fs.readFileSync(filePath, 'utf8');
|
|
360
|
+
} catch (err) {
|
|
361
|
+
process.stderr.write(`qa_red_lint: cannot read ${filePath}: ${err.message}\n`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const rule of RULES) {
|
|
366
|
+
let ruleFlags;
|
|
367
|
+
try {
|
|
368
|
+
ruleFlags = rule.scan(fileText);
|
|
369
|
+
} catch {
|
|
370
|
+
// Never crash on malformed input — rule scan failure is non-fatal
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
for (const flag of ruleFlags) {
|
|
374
|
+
process.stderr.write(`${filePath}:${flag.line}: [${rule.id}] ${flag.message}\n`);
|
|
375
|
+
totalFlags++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
process.exit(totalFlags > 0 ? 1 : 0);
|
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
# AGENT_TYPE — populates incident JSON agent_type field (null if empty)
|
|
26
26
|
# WORK_ITEM_ID — populates incident JSON work_item_id field (null if empty)
|
|
27
27
|
# RUN_SCRIPT_ACTIVE — self-exemption guard (set to 1 by this wrapper)
|
|
28
|
+
# CLEARGATE_STATE_FILE — explicitly forwarded/exported to child (F8, CR-080)
|
|
29
|
+
# CLAUDE_PROJECT_DIR — explicitly forwarded/exported to child (F8, CR-080)
|
|
30
|
+
# RUN_SCRIPT_ENV_ALLOWLIST — optional: comma/space-separated list of env var names;
|
|
31
|
+
# when set, only those vars (plus the always-forwarded set
|
|
32
|
+
# above) are guaranteed exported to the child. Default:
|
|
33
|
+
# full inherited-environment pass-through (allowlist is
|
|
34
|
+
# opt-in only — do NOT set this unless you need isolation).
|
|
28
35
|
|
|
29
36
|
set -euo pipefail
|
|
30
37
|
|
|
@@ -32,7 +39,13 @@ set -euo pipefail
|
|
|
32
39
|
# Self-exemption guard — do not wrap recursively
|
|
33
40
|
# ---------------------------------------------------------------------------
|
|
34
41
|
if [[ "${RUN_SCRIPT_ACTIVE:-}" == "1" ]]; then
|
|
35
|
-
# Already inside a wrapper invocation; pass through directly, no JSON capture
|
|
42
|
+
# Already inside a wrapper invocation; pass through directly, no JSON capture.
|
|
43
|
+
# F8 (CR-080): guarantee ClearGate config vars are exported even on this fast path.
|
|
44
|
+
[[ -n "${CLEARGATE_STATE_FILE:-}" ]] && export CLEARGATE_STATE_FILE
|
|
45
|
+
[[ -n "${ORCHESTRATOR_PROJECT_DIR:-}" ]] && export ORCHESTRATOR_PROJECT_DIR
|
|
46
|
+
[[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && export CLAUDE_PROJECT_DIR
|
|
47
|
+
[[ -n "${AGENT_TYPE:-}" ]] && export AGENT_TYPE
|
|
48
|
+
[[ -n "${WORK_ITEM_ID:-}" ]] && export WORK_ITEM_ID
|
|
36
49
|
exec "$@"
|
|
37
50
|
fi
|
|
38
51
|
|
|
@@ -88,6 +101,26 @@ trap 'rm -f "$STDOUT_TMP" "$STDERR_TMP"' EXIT
|
|
|
88
101
|
# Mark self as active before running the wrapped command
|
|
89
102
|
export RUN_SCRIPT_ACTIVE=1
|
|
90
103
|
|
|
104
|
+
# F8 (CR-080): explicitly export the documented ClearGate config vars so they
|
|
105
|
+
# reach the child regardless of whether the caller used `export` or plain assignment.
|
|
106
|
+
# Default = full inherited-environment pass-through; allowlist is opt-in only.
|
|
107
|
+
[[ -n "${CLEARGATE_STATE_FILE:-}" ]] && export CLEARGATE_STATE_FILE
|
|
108
|
+
[[ -n "${ORCHESTRATOR_PROJECT_DIR:-}" ]] && export ORCHESTRATOR_PROJECT_DIR
|
|
109
|
+
[[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && export CLAUDE_PROJECT_DIR
|
|
110
|
+
[[ -n "${AGENT_TYPE:-}" ]] && export AGENT_TYPE
|
|
111
|
+
[[ -n "${WORK_ITEM_ID:-}" ]] && export WORK_ITEM_ID
|
|
112
|
+
|
|
113
|
+
# Optional allowlist: when RUN_SCRIPT_ENV_ALLOWLIST is set (comma/space-separated
|
|
114
|
+
# var names), restrict env to only those vars + the always-forwarded set above.
|
|
115
|
+
# This is opt-in isolation — never the default.
|
|
116
|
+
if [[ -n "${RUN_SCRIPT_ENV_ALLOWLIST:-}" ]]; then
|
|
117
|
+
_allowed_env=""
|
|
118
|
+
for _var in ${RUN_SCRIPT_ENV_ALLOWLIST//,/ }; do
|
|
119
|
+
_val="${!_var:-}"
|
|
120
|
+
[[ -n "$_val" ]] && _allowed_env="${_allowed_env}${_var}=${_val} "
|
|
121
|
+
done
|
|
122
|
+
fi
|
|
123
|
+
|
|
91
124
|
EXIT_CODE=0
|
|
92
125
|
"$@" >"$STDOUT_TMP" 2>"$STDERR_TMP" || EXIT_CODE=$?
|
|
93
126
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cr077_eviction.red.sh — CR-077 QA-Red eviction grep test.
|
|
3
|
+
#
|
|
4
|
+
# RED: fails against clean baseline — the EPIC-028 policy tokens (node:test,
|
|
5
|
+
# vitest, check:no-vitest, tsx --test, node.test.ts) are still present in the
|
|
6
|
+
# three scoped agent files + gate-checks.json + sprint_context.md template.
|
|
7
|
+
# GREEN: passes after Developer evicts those policy tokens (M1 §5b / §7.2).
|
|
8
|
+
#
|
|
9
|
+
# SCOPE NARROWED per M1 plan §5b + §7.2:
|
|
10
|
+
# - Grep ONLY the three §3-scoped agents + gate-checks.json + sprint_context.md
|
|
11
|
+
# (NOT bare 'cleargate-cli' token — it legitimately appears in devops/reporter/
|
|
12
|
+
# wiki agents as meta-repo path references and in qa.md:98/116 + architect.md:64
|
|
13
|
+
# as meta-repo runtime-lane heuristics; asserting zero there forces deleting
|
|
14
|
+
# correct framework content — see M1 plan §7 Risk 2).
|
|
15
|
+
# - The gate-checks.json IS in scope because its `cd cleargate-cli` literal IS
|
|
16
|
+
# the F6 leak.
|
|
17
|
+
#
|
|
18
|
+
# Mirrors the test_prep_qa_context.sh bash-harness pattern
|
|
19
|
+
# (.cleargate/scripts/test/ shape, mktemp-free since we read live files).
|
|
20
|
+
# macOS bash 3.2 portable.
|
|
21
|
+
#
|
|
22
|
+
# Exit 0 = PASS (all assertions pass); exit 1 = one or more FAIL.
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
26
|
+
|
|
27
|
+
PASS=0
|
|
28
|
+
FAIL=0
|
|
29
|
+
|
|
30
|
+
pass() {
|
|
31
|
+
echo "PASS: $1"
|
|
32
|
+
PASS=$((PASS + 1))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fail() {
|
|
36
|
+
echo "FAIL: $1"
|
|
37
|
+
echo " detail: $2"
|
|
38
|
+
FAIL=$((FAIL + 1))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ── File paths (absolute) ─────────────────────────────────────────────────────
|
|
42
|
+
DEVELOPER_MD="${REPO_ROOT}/cleargate-planning/.claude/agents/developer.md"
|
|
43
|
+
QA_MD="${REPO_ROOT}/cleargate-planning/.claude/agents/qa.md"
|
|
44
|
+
ARCHITECT_MD="${REPO_ROOT}/cleargate-planning/.claude/agents/architect.md"
|
|
45
|
+
GATE_CHECKS="${REPO_ROOT}/cleargate-planning/.cleargate/scripts/gate-checks.json"
|
|
46
|
+
SPRINT_CONTEXT_TEMPLATE="${REPO_ROOT}/cleargate-planning/.cleargate/templates/sprint_context.md"
|
|
47
|
+
|
|
48
|
+
# ── Guard: verify scoped files exist ─────────────────────────────────────────
|
|
49
|
+
for f in "$DEVELOPER_MD" "$QA_MD" "$ARCHITECT_MD" "$GATE_CHECKS" "$SPRINT_CONTEXT_TEMPLATE"; do
|
|
50
|
+
if [[ ! -f "$f" ]]; then
|
|
51
|
+
fail "scoped file exists" "MISSING: $f"
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
# ── Scenario: EPIC-028 policy tokens evicted from scoped files ───────────────
|
|
56
|
+
#
|
|
57
|
+
# Tokens: node:test | vitest | check:no-vitest | tsx --test | node.test.ts
|
|
58
|
+
# (the bare 'cleargate-cli' token is EXCLUDED from this grep — see SCOPE note above)
|
|
59
|
+
#
|
|
60
|
+
# Expected result after Developer implements CR-077: ZERO matches.
|
|
61
|
+
# Right now (clean baseline) this returns matches → the test FAILS RED.
|
|
62
|
+
|
|
63
|
+
MATCH_COUNT=$(grep -rniE \
|
|
64
|
+
"node:test|vitest|check:no-vitest|tsx --test|node\.test\.ts" \
|
|
65
|
+
"$DEVELOPER_MD" \
|
|
66
|
+
"$QA_MD" \
|
|
67
|
+
"$ARCHITECT_MD" \
|
|
68
|
+
"$GATE_CHECKS" \
|
|
69
|
+
"$SPRINT_CONTEXT_TEMPLATE" \
|
|
70
|
+
2>/dev/null | wc -l | tr -d '[:space:]')
|
|
71
|
+
|
|
72
|
+
if [[ "$MATCH_COUNT" -eq 0 ]]; then
|
|
73
|
+
pass "eviction grep — zero EPIC-028 policy tokens in scoped files"
|
|
74
|
+
else
|
|
75
|
+
# Print the actual matches to help the Developer understand what remains
|
|
76
|
+
MATCH_LINES=$(grep -rniE \
|
|
77
|
+
"node:test|vitest|check:no-vitest|tsx --test|node\.test\.ts" \
|
|
78
|
+
"$DEVELOPER_MD" \
|
|
79
|
+
"$QA_MD" \
|
|
80
|
+
"$ARCHITECT_MD" \
|
|
81
|
+
"$GATE_CHECKS" \
|
|
82
|
+
"$SPRINT_CONTEXT_TEMPLATE" \
|
|
83
|
+
2>/dev/null | head -20)
|
|
84
|
+
fail "eviction grep — EPIC-028 policy tokens still present (expect ZERO, got ${MATCH_COUNT})" \
|
|
85
|
+
"First matches:
|
|
86
|
+
${MATCH_LINES}"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# ── Sub-scenario: gate-checks.json specifically has no "cd cleargate-cli" ────
|
|
90
|
+
# The F6 literal must be gone from the shipped payload gate-checks.json.
|
|
91
|
+
if ! grep -q "cd cleargate-cli" "$GATE_CHECKS" 2>/dev/null; then
|
|
92
|
+
pass "gate-checks.json — no 'cd cleargate-cli' F6 literal in shipped payload"
|
|
93
|
+
else
|
|
94
|
+
fail "gate-checks.json — F6 literal 'cd cleargate-cli' still present (count: $(grep -c 'cd cleargate-cli' "$GATE_CHECKS"))" \
|
|
95
|
+
"$(grep -n 'cd cleargate-cli' "$GATE_CHECKS")"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ── Sub-scenario: sprint_context.md template has a ## Test Stack block ───────
|
|
99
|
+
# The template must contain the structured test-stack block introduced by CR-077 §3b.
|
|
100
|
+
if grep -q "## Test Stack" "$SPRINT_CONTEXT_TEMPLATE" 2>/dev/null; then
|
|
101
|
+
pass "sprint_context.md template — ## Test Stack block present"
|
|
102
|
+
else
|
|
103
|
+
fail "sprint_context.md template — ## Test Stack block MISSING" \
|
|
104
|
+
"grep '## Test Stack' returned no match in ${SPRINT_CONTEXT_TEMPLATE}"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
108
|
+
echo ""
|
|
109
|
+
echo "cr077_eviction.red.sh: ${PASS} passed, ${FAIL} failed"
|
|
110
|
+
if [[ "$FAIL" -gt 0 ]]; then
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
exit 0
|