leerness 1.36.187 → 1.36.188
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 +4 -0
- package/README.ko.md +2 -0
- package/README.md +6 -6
- package/bin/leerness.js +26 -6
- package/docs/state-paths-api.md +5 -0
- package/docs/store-diagnostics-api.md +67 -0
- package/docs/store-observation-contract.md +94 -0
- package/docs/verification-evidence.md +67 -0
- package/lib/store-diagnostic-read.js +66 -0
- package/lib/store-diagnostics.js +74 -0
- package/package.json +3 -2
- package/scripts/command-flags-probe.js +3 -2
- package/scripts/store-diagnostic-probe.js +272 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.188 — 2026-09-08
|
|
4
|
+
|
|
5
|
+
- Add read-only state stores diagnostics for decision, lesson and role status, bounded reads and explicit fallback provenance; preserve metadata-only state inspect and existing mutation behavior.
|
|
6
|
+
|
|
3
7
|
## 1.36.187 — 2026-09-05
|
|
4
8
|
|
|
5
9
|
- feat(state): add read-only runtime compatibility diagnosis and observed-layout write guards; keep legacy storage without migration or activation
|
package/README.ko.md
CHANGED
|
@@ -90,6 +90,8 @@ leerness verify-claim T-0001 --require-evidence
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
- `optimism-check` · `verify-claim` — evidence의 도메인 주장(API·DB·결제·이메일·큐·캐시·알림·스토리지 등 10종)을 실제 소스 호출과 대조. **JavaScript뿐 아니라 Python·Ruby·Go·C#·Java·PHP·Rust 구현도 인식**합니다(1.13).
|
|
93
|
+
|
|
94
|
+
`verify-claim`의 통과는 적용된 증거 검사 통과이며 요구사항의 의미적 정확성 보장이 아닙니다. 단건 `--json`의 `evidenceLevel: "executed"`에는 실패한 실행도 포함되고 `semanticVerified`는 항상 `false`입니다. `--run-tests`를 지정해도 명령이 없거나 차단되면 실행을 건너뜁니다. `ok`·`reasons`·`run.skipped`·`run.allPassed`를 함께 확인하세요. [검증 결과 해석 및 실행 예제](docs/verification-evidence.md)를 참고하세요.
|
|
93
95
|
- `lazy detect` — 증거 없는 done, 빈 handoff, 테스트 미실행, 미추적 TODO를 탐지.
|
|
94
96
|
- 정직한 완료는 통과하고 가짜 완료만 exit 1로 차단 — `gate` 또는 CI에 그대로 연결됩니다.
|
|
95
97
|
|
package/README.md
CHANGED
|
@@ -39,9 +39,9 @@ npx leerness task update T-0002 --status done --evidence "payment.js implemented
|
|
|
39
39
|
npx leerness verify-claim T-0002 # exit 1 — payment.js does not exist. Claim rejected.
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Now
|
|
42
|
+
Now write the implementation and record evidence, then run the **same** `verify-claim T-0002` again. A successful check means the applicable evidence checks passed; creating a file alone does not prove that the payment API works.
|
|
43
43
|
|
|
44
|
-
> Tip:
|
|
44
|
+
> Tip: test counts are static declaration-pattern heuristics, not execution or coverage proof. Add `--run-tests --test-cmd "<your trusted test command>" --json` to check the selected command's exit status and supported reporter output. `evidenceLevel: "executed"` includes failed runs; inspect `ok`, `reasons`, and `run` together. `semanticVerified` remains `false`. See [verification evidence and runnable examples](docs/verification-evidence.md) for skipped runs, unparsed output, and reporting limits.
|
|
45
45
|
|
|
46
46
|
> Want a smaller footprint? `leerness init . --minimal` installs only the core memory + verification files instead of the full set.
|
|
47
47
|
|
|
@@ -166,7 +166,7 @@ MIT
|
|
|
166
166
|
<!-- leerness:project-readme:start -->
|
|
167
167
|
## Leerness Project Harness
|
|
168
168
|
|
|
169
|
-
이 프로젝트는 Leerness v1.36.
|
|
169
|
+
이 프로젝트는 Leerness v1.36.188 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
170
170
|
|
|
171
171
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
172
172
|
|
|
@@ -220,7 +220,7 @@ leerness memory restore decision <date|title>
|
|
|
220
220
|
|
|
221
221
|
### MCP server (외부 AI 통합)
|
|
222
222
|
|
|
223
|
-
Leerness v1.36.
|
|
223
|
+
Leerness v1.36.188는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **98개 도구**를 노출:
|
|
224
224
|
|
|
225
225
|
```jsonc
|
|
226
226
|
// 카테고리별
|
|
@@ -241,7 +241,7 @@ Leerness v1.36.187는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
|
|
|
241
241
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
242
242
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
243
243
|
|
|
244
|
-
현재 누적: **v1.9.x → 1.36.
|
|
244
|
+
현재 누적: **v1.9.x → 1.36.188 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
245
245
|
|
|
246
246
|
### 성능 가이드
|
|
247
247
|
|
|
@@ -279,5 +279,5 @@ leerness release pack --close --auto-main-push
|
|
|
279
279
|
- `.leerness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
280
280
|
- `.leerness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
281
281
|
|
|
282
|
-
Last synced by Leerness v1.36.
|
|
282
|
+
Last synced by Leerness v1.36.188: 2026-09-08
|
|
283
283
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -50,7 +50,7 @@ const {
|
|
|
50
50
|
migrateLegacyWorkspace,
|
|
51
51
|
} = require('../lib/workspace-dir');
|
|
52
52
|
|
|
53
|
-
const VERSION = '1.36.
|
|
53
|
+
const VERSION = '1.36.188';
|
|
54
54
|
|
|
55
55
|
// MCP lifecycle 주소 표식은 현재 CLI 호출 한 번에만 유효하다. CLI bootstrap에서 즉시 env에서
|
|
56
56
|
// 떼어 두어 `--no-record`/hook처럼 presence 기록 함수에 도달하지 않는 경로도 후속 child에 유출하지 않는다.
|
|
@@ -893,7 +893,7 @@ function _errorUiLang(root, command) {
|
|
|
893
893
|
const parsed = nonFlagArgs();
|
|
894
894
|
const resolvedCommand = command || parsed[0];
|
|
895
895
|
if (resolvedCommand === 'mode' && has('--json')) return 'ko';
|
|
896
|
-
if (resolvedCommand === 'state' && ['inspect', 'compatibility'].includes(parsed[1])) return _inspectionUiLang();
|
|
896
|
+
if (resolvedCommand === 'state' && ['inspect', 'compatibility', 'stores'].includes(parsed[1])) return _inspectionUiLang();
|
|
897
897
|
return _uiLang(resolvedCommand === 'mode' ? _modeRoot(parsed, root) : root);
|
|
898
898
|
}
|
|
899
899
|
function _inspectionUiLang() {
|
|
@@ -11563,6 +11563,7 @@ function commandsCmd(root) {
|
|
|
11563
11563
|
{ cmd: 'state show|start|record|verify|handoff', desc: '.leerness/ JSON 상태 substrate (에이전트 간 인수인계 표준) — 1.9.278', descEn: '.leerness/ JSON state substrate (cross-agent handoff standard) — 1.9.278' },
|
|
11564
11564
|
{ cmd: 'state inspect [path] [--json]', desc: '5-scope 현재/제안 경로와 기존 상태 목록 — 읽기 전용, 이동·활성화 없음', descEn: 'five-scope current/proposed paths and legacy inventory — read-only, no migration or activation' },
|
|
11565
11565
|
{ cmd: 'state compatibility [path] [--json]', desc: '관측한 runtime layout의 쓰기 호환성 — 읽기 전용, 이동·활성화 없음', descEn: 'observed runtime-layout write compatibility — read-only, no migration or activation' },
|
|
11566
|
+
{ cmd: 'state stores [path] [--json]', desc: '결정·교훈·역할 저장소 상태와 fallback — 읽기 전용, 본문 비노출', descEn: 'decision, lesson and role store status and fallback — read-only, no payload output' },
|
|
11566
11567
|
{ cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280', descEn: 'generate per-tool instructions/.mcp.json selectively (claude/cursor/codex/goose/...) — 1.9.280' },
|
|
11567
11568
|
{ cmd: 'ci init [path] [--force]', desc: 'PR 마다 leerness gate 실행하는 GitHub Actions 워크플로 생성 (.github/workflows/leerness-gate.yml) — 1.9.444', descEn: 'create a GitHub Actions workflow that runs leerness gate on every PR (.github/workflows/leerness-gate.yml) — 1.9.444' },
|
|
11568
11569
|
{ cmd: 'export|prompt --target <agent>', desc: 'adapter 별칭 — 도구별 지침/계약 파일 생성 (claude/cursor/codex/agents-md/...) — 1.9.448', descEn: 'adapter alias — generate per-tool instruction/contract files (claude/cursor/codex/agents-md/...) — 1.9.448' },
|
|
@@ -12257,7 +12258,7 @@ const _OBSERVATION_ONLY_SUBCOMMANDS = new Map([
|
|
|
12257
12258
|
['lease', new Set(['list', 'check'])],
|
|
12258
12259
|
['roles', new Set(['validate'])],
|
|
12259
12260
|
['role', new Set(['validate'])],
|
|
12260
|
-
['state', new Set(['inspect', 'compatibility'])],
|
|
12261
|
+
['state', new Set(['inspect', 'compatibility', 'stores'])],
|
|
12261
12262
|
]);
|
|
12262
12263
|
function _cliMutationClass(args, cmd) {
|
|
12263
12264
|
if (cmd === 'mcp') return 'observation-only'; // server startup must not mutate its incidental cwd
|
|
@@ -15709,6 +15710,7 @@ const _STRICT_COMMAND_FLAGS = {
|
|
|
15709
15710
|
'roles validate': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles validate [--path .] [--json]' },
|
|
15710
15711
|
'state inspect': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'state inspect [path] [--path <path>] [--json]' },
|
|
15711
15712
|
'state compatibility': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'state compatibility [path] [--path <path>] [--json]' },
|
|
15713
|
+
'state stores': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'state stores [path] [--path <path>] [--json]' },
|
|
15712
15714
|
'roles set': { allowed: ['--provider', '--to', '--model', '--model-family', '--policy', '--fallback-policy', '--candidate', '--candidate-family', '--remove-candidate', '--clear-candidates', '--persona', '--force'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles set <role> --provider <id> [--model <id>] [--policy strict|balanced|continuity]' },
|
|
15713
15715
|
'roles unset': { allowed: [], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles unset <role> [--path .] [--json]' },
|
|
15714
15716
|
'roles suggest': { allowed: ['--apply'], globals: [..._COMMON_COMMAND_FLAGS, '--json'], usage: 'roles suggest [--apply] [--path .] [--json]' },
|
|
@@ -15790,7 +15792,7 @@ function _validateRolesCommandShape(cmd, args) {
|
|
|
15790
15792
|
}
|
|
15791
15793
|
|
|
15792
15794
|
function _validateCommandFlags(cmd, args) {
|
|
15793
|
-
if (cmd === 'state' && ['inspect', 'compatibility'].includes(args[1])) {
|
|
15795
|
+
if (cmd === 'state' && ['inspect', 'compatibility', 'stores'].includes(args[1])) {
|
|
15794
15796
|
if (!_rejectDuplicateValueFlags(['--path'])) return false;
|
|
15795
15797
|
if (args.length > 3) {
|
|
15796
15798
|
failJson(has('--json'), 'too_many_arguments', `Usage: state ${args[1]} [path] [--path <path>] [--json]`);
|
|
@@ -15809,7 +15811,7 @@ function _validateCommandFlags(cmd, args) {
|
|
|
15809
15811
|
if (cmd === 'intent' && args[1] === 'expand') route = 'intent expand';
|
|
15810
15812
|
if (cmd === 'lease') route = `lease ${args[1] == null ? 'list' : args[1]}`;
|
|
15811
15813
|
if (roleSub) route = `roles ${roleSub === 'remove' || roleSub === 'rm' ? 'unset' : roleSub}`;
|
|
15812
|
-
if (cmd === 'state' && ['inspect', 'compatibility'].includes(args[1])) route = `state ${args[1]}`;
|
|
15814
|
+
if (cmd === 'state' && ['inspect', 'compatibility', 'stores'].includes(args[1])) route = `state ${args[1]}`;
|
|
15813
15815
|
const cfg = _STRICT_COMMAND_FLAGS[route];
|
|
15814
15816
|
if (!cfg) return true;
|
|
15815
15817
|
return _rejectUnknownFlags(cfg.allowed, cfg.usage, { globals: cfg.globals });
|
|
@@ -29077,6 +29079,18 @@ function stateCompatibilityCmd(root) {
|
|
|
29077
29079
|
return report;
|
|
29078
29080
|
}
|
|
29079
29081
|
|
|
29082
|
+
function stateStoresCmd(root) {
|
|
29083
|
+
try {
|
|
29084
|
+
const { inspectStores, formatStoreDiagnostics } = require('../lib/store-diagnostics');
|
|
29085
|
+
const report = inspectStores(root);
|
|
29086
|
+
log(has('--json') ? JSON.stringify(report, null, 2) : formatStoreDiagnostics(report));
|
|
29087
|
+
if (!report.ok) process.exitCode = 1;
|
|
29088
|
+
return report;
|
|
29089
|
+
} catch {
|
|
29090
|
+
return failJson(has('--json'), 'store_diagnostics_failed', 'Store diagnostics could not be completed; no files were changed.');
|
|
29091
|
+
}
|
|
29092
|
+
}
|
|
29093
|
+
|
|
29080
29094
|
// leerness state <show|start|record|verify|handoff>
|
|
29081
29095
|
// 1.9.292 (UR-0031): get_project_context — 외부 에이전트 온보딩용 단일 집약 컨텍스트.
|
|
29082
29096
|
// 1콜로 현재 작업/미답 요청/최근 결정/활성 룰/next-actions/memory/프로젝트 의도를 구조화 회수.
|
|
@@ -31986,6 +32000,7 @@ STATUS & DIAGNOSTICS
|
|
|
31986
32000
|
status [path] Install status (files present)
|
|
31987
32001
|
state inspect [path] [--json] Read-only five-scope paths/inventory; no migration or runtime activation
|
|
31988
32002
|
state compatibility [path] [--json] Read-only observed-layout write admission; no activation
|
|
32003
|
+
state stores [path] [--json] Read-only decision/lesson/role diagnostics; no payload output
|
|
31989
32004
|
health [path] | doctor [--json] health: project state · doctor: CLI install/environment (no path arg)
|
|
31990
32005
|
verify [path] Required-file verification
|
|
31991
32006
|
which [--json] Resolve current binary/version (npm cache conflicts)
|
|
@@ -32069,6 +32084,7 @@ function help() {
|
|
|
32069
32084
|
leerness lease acquire|release|list|check [file|lease-id] [--session KEY] [--ttl SEC] [--json] # exact-file opt-in 협업 lease
|
|
32070
32085
|
leerness state inspect [path] [--json] # 5-scope 경로/목록 읽기 전용 진단 — 이동·활성화 없음
|
|
32071
32086
|
leerness state compatibility [path] [--json] # 관측 layout 쓰기 호환성 읽기 전용 진단 — 활성화 없음
|
|
32087
|
+
leerness state stores [path] [--json] # 결정/교훈/역할 상태·fallback 진단 — 읽기 전용, 본문 비노출
|
|
32072
32088
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
32073
32089
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
32074
32090
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -32115,6 +32131,10 @@ async function main(runtimeEntered = false) {
|
|
|
32115
32131
|
const explicit = arg('--path', null);
|
|
32116
32132
|
return stateCompatibilityCmd(explicit !== null ? explicit : args[2] === undefined ? process.cwd() : args[2]);
|
|
32117
32133
|
}
|
|
32134
|
+
if (cmd === 'state' && args[1] === 'stores') {
|
|
32135
|
+
const explicit = arg('--path', null);
|
|
32136
|
+
return stateStoresCmd(explicit !== null ? explicit : args[2] === undefined ? process.cwd() : args[2]);
|
|
32137
|
+
}
|
|
32118
32138
|
const runtimeRoot = _runtimeProjectRoot(args, cmd);
|
|
32119
32139
|
if (!runtimeEntered && runtimeRoot && !_mustRemainReadOnly(args, cmd)) {
|
|
32120
32140
|
return withRuntimeWrites(runtimeRoot, () => main(true));
|
|
@@ -33296,7 +33316,7 @@ async function main(runtimeEntered = false) {
|
|
|
33296
33316
|
|
|
33297
33317
|
const skillCatalog = (() => {
|
|
33298
33318
|
const args = nonFlagArgs();
|
|
33299
|
-
if (require.main === module && args[0] === 'state' && ['inspect', 'compatibility'].includes(args[1])) {
|
|
33319
|
+
if (require.main === module && args[0] === 'state' && ['inspect', 'compatibility', 'stores'].includes(args[1])) {
|
|
33300
33320
|
return _withBuiltinSource(BUILTIN_CATALOG);
|
|
33301
33321
|
}
|
|
33302
33322
|
return _loadSkillCatalog();
|
package/docs/state-paths-api.md
CHANGED
|
@@ -4,6 +4,11 @@ Available in v1.36.186. T-0174 / P-0020 is an additive inspection-only foundatio
|
|
|
4
4
|
It does not move data or activate a new backend. The complete staged design is in
|
|
5
5
|
[State scopes and migration](state-scopes.md).
|
|
6
6
|
|
|
7
|
+
This API inventories path metadata, not payload validity. A successful inspection
|
|
8
|
+
does not imply that the stores contain valid JSON or domain records. See the
|
|
9
|
+
[store observation boundaries and staged contract](store-observation-contract.md)
|
|
10
|
+
for existing validator differences, and the [separate store diagnostic candidate](store-diagnostics-api.md).
|
|
11
|
+
|
|
7
12
|
## CLI
|
|
8
13
|
|
|
9
14
|
```sh
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Store diagnostics API v1
|
|
2
|
+
|
|
3
|
+
Implementation candidate for T-0192 / P-0022. Publication is tracked separately.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
leerness state stores ./project --json
|
|
7
|
+
leerness state stores --path ./project
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This separate command reads only decisions, lessons and roles in the selected
|
|
11
|
+
workspace. `state inspect` remains metadata-only. Explicit `--path` takes precedence
|
|
12
|
+
over the optional positional directory. Duplicate paths, extra positional arguments
|
|
13
|
+
and unsupported write flags fail before ordinary startup bookkeeping.
|
|
14
|
+
|
|
15
|
+
`inspectStores(root, options)` is exported by `lib/store-diagnostics.js`; options
|
|
16
|
+
are passed to the existing `resolveStatePaths` resolver. It resolves the selected
|
|
17
|
+
workspace, not all worktrees or every native runtime authority. A legacy workspace
|
|
18
|
+
is diagnosed in place without migration. No provider or model call is made.
|
|
19
|
+
|
|
20
|
+
## Result
|
|
21
|
+
|
|
22
|
+
The single JSON document has schema `leerness.store-diagnostics/v1`, `ok`,
|
|
23
|
+
`readOnly:true`, `writesPerformed:false`, `workspace`, `warnings`, and three `stores`.
|
|
24
|
+
Each row identifies the fixed filename and contains:
|
|
25
|
+
|
|
26
|
+
- `canonical.status`: `valid`, `missing`, `invalid_json`, `invalid_shape`,
|
|
27
|
+
`unreadable`, `unsupported`, or `unknown`.
|
|
28
|
+
- `reasonCode` and actual `bytesRead` inside `canonical`; no original text or parse
|
|
29
|
+
error excerpt is included. `syntaxValid`, `shapeValid`, `itemsValid` are tri-state.
|
|
30
|
+
- `effectiveSource`: source selected by this diagnostic (`canonical`,
|
|
31
|
+
`legacy_markdown`, `none`), not an authorization to write or switch runtime authority.
|
|
32
|
+
- `fallback`: whether the diagnostic attempted/used bounded Markdown parsing, its
|
|
33
|
+
status and (when attempted) read metadata. It does not execute the legacy loader.
|
|
34
|
+
- `itemCount`: parsed array/Markdown count or validated role count, otherwise null.
|
|
35
|
+
|
|
36
|
+
Missing optional data is allowed. An invalid canonical file keeps `ok:false` even
|
|
37
|
+
when Markdown is available. CLI exit is 0 only when `ok:true`; failed discovery
|
|
38
|
+
returns a generic `store_diagnostics_failed` envelope and exit 1. For each memory
|
|
39
|
+
store, fallback is attempted only on missing, syntax-error or non-array JSON.
|
|
40
|
+
Unreadable, unsupported and changing sources do not trigger fallback. This is
|
|
41
|
+
deliberately stricter than the existing resilient memory loader, which is unchanged.
|
|
42
|
+
|
|
43
|
+
Memory JSON validates array shape only: `itemsValid:null` even for an empty array.
|
|
44
|
+
This is not full item validation or a semantic-health certificate. Roles reuse the
|
|
45
|
+
pure legacy role schema and extended role-definition validator. Markdown uses the
|
|
46
|
+
existing pure parsers; readable text with no matching entries can yield count zero.
|
|
47
|
+
|
|
48
|
+
## Read and scope limits
|
|
49
|
+
|
|
50
|
+
Each file is limited to 1 MiB; the reader consumes at most limit+1 bytes to detect
|
|
51
|
+
growth/overflow. Size checks alone are not relied on. It uses strict UTF-8 and
|
|
52
|
+
rejects observed linked/non-regular/multiply-linked files and linked workspace
|
|
53
|
+
parents. File/parent identity and file metadata are compared around the read;
|
|
54
|
+
observed changes fail rather than certify an unstable snapshot. These checks are
|
|
55
|
+
not a portable filesystem transaction, writer lease or guarantee against every
|
|
56
|
+
hostile concurrent filesystem action.
|
|
57
|
+
|
|
58
|
+
Only selected known files are covered, with no recursive scan, automatic repair,
|
|
59
|
+
archive append, usage write, presence update or migration. The diagnostic performs
|
|
60
|
+
no content writes; ordinary filesystem access-time effects are not a content/mtime
|
|
61
|
+
immutability guarantee. Existing mutation commands and their late save guards are
|
|
62
|
+
unchanged. See [the broader observation contract](store-observation-contract.md).
|
|
63
|
+
|
|
64
|
+
Run `npm run test:store-diagnostics` for the focused functional checks. This suite
|
|
65
|
+
also runs through `test:state-scopes`, hence through the existing fast/full gates.
|
|
66
|
+
Its ordinary fixtures test schema/fallback/size/encoding/privacy and CLI zero-write
|
|
67
|
+
behavior; it is not an adversarial filesystem or OS-permission coverage claim.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Store observation: current boundaries and staged changes
|
|
2
|
+
|
|
3
|
+
T-0187-B / T-0191. Source baseline: product code at `70b2a124`; the later
|
|
4
|
+
`7694fb8` commit changes verification documentation only. This is a bounded
|
|
5
|
+
inventory and implementation contract, not a claim that the proposed API exists.
|
|
6
|
+
|
|
7
|
+
Update: the T-0192 implementation candidate adds a separate `state stores` command.
|
|
8
|
+
See [its exact API and remaining limits](store-diagnostics-api.md). The baseline
|
|
9
|
+
table below describes pre-existing behavior; mutation admission and migration B
|
|
10
|
+
remain unimplemented follow-ups.
|
|
11
|
+
|
|
12
|
+
## Current behavior
|
|
13
|
+
|
|
14
|
+
| Surface | What it observes | Missing / invalid / unreadable behavior | Boundary |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `state inspect` / `inspectLegacyInventory` | Known paths, file kinds, sizes and times | `absent`, `unreadable`, or blocked parent metadata | Does not read payloads; `ok:true` does not validate store contents |
|
|
17
|
+
| `findCorruptedStateJson` used by check/health/audit | Nonempty, readable, top-level `.leerness/*.json` syntax | Missing or unreadable directory returns no findings; unreadable/blank files are skipped | No recursive scan or schema validation; no findings is not proof every store was checked |
|
|
18
|
+
| `readRoleStore` / `roles validate` | Role file boundaries, size checks before and after a whole-file read, strict UTF-8, JSON and role schema | Missing is explicit; malformed JSON, invalid shape and unreadable state are rejected separately | The read itself has no byte cap; this role schema does not apply to other stores |
|
|
19
|
+
| `_loadDecisions`, `_loadLessons` | JSON array, otherwise Markdown fallback | Missing, malformed JSON, non-array JSON and caught JSON read errors can all fall back | Returned array does not identify the chosen source or canonical error; items are not fully schema-validated |
|
|
20
|
+
| `_assertStoreParsable` | Existing JSON syntax at a save boundary | Parse/read failure becomes `E_STORE_CORRUPT`; valid JSON with wrong shape passes | A late save guard is not a multi-file transaction |
|
|
21
|
+
| `state compatibility` | Fixed runtime layout descriptor and admission conditions | Missing compatible legacy layout differs from unsupported/unreadable conditions via `reasonCode` | Compatibility-only; no payload migration, runtime activation or memory validation |
|
|
22
|
+
|
|
23
|
+
Sources: [inventory](../lib/state-inventory.js), [inspection](../lib/state-inspect.js),
|
|
24
|
+
[integrity](../lib/state-integrity.js), [roles](../lib/role-store.js),
|
|
25
|
+
[runtime layout](../lib/runtime-layout.js), and the `_loadDecisions`, `_loadLessons`,
|
|
26
|
+
`_assertStoreParsable`, `lessonDropCmd`, `decisionDropCmd`, `memoryRestoreCmd`
|
|
27
|
+
functions in [the CLI](../bin/leerness.js). Function names are navigation anchors,
|
|
28
|
+
not a promise that these internal functions are public exports.
|
|
29
|
+
|
|
30
|
+
### Legacy memory needs separate read and write contracts
|
|
31
|
+
|
|
32
|
+
- A readable JSON array wins over Markdown, even if individual items are unsuitable.
|
|
33
|
+
- A non-array JSON document is parseable but is not a valid memory array; the loader
|
|
34
|
+
falls back while the syntax-only save guard permits it.
|
|
35
|
+
- Blank JSON falls back on read and fails the save guard, yet the integrity scan skips it.
|
|
36
|
+
- JSON read exceptions are caught by the loader; Markdown read exceptions are not.
|
|
37
|
+
`existsSync` false is not a positively observed `ENOENT` reason.
|
|
38
|
+
- Add/save can exit on deduplication before reaching the save guard. A successful
|
|
39
|
+
no-op therefore does not certify the canonical file.
|
|
40
|
+
- Drop appends to the archive before the final save guard. Restore appends to active
|
|
41
|
+
Markdown before the final save guard. Refusing the canonical save does not undo
|
|
42
|
+
those earlier writes. JSON then Markdown saves are sequential, not transactional.
|
|
43
|
+
|
|
44
|
+
The ordering and fallback statements above are source observations, not destructive
|
|
45
|
+
tests against user data. Do not activate migration B assuming zero mutation on failure.
|
|
46
|
+
|
|
47
|
+
## Proposed observation contract — not implemented
|
|
48
|
+
|
|
49
|
+
Keep `state inspect` metadata-only. Add a separately requested, read-only store
|
|
50
|
+
diagnostic surface in a later implementation; do not silently add payload reads to
|
|
51
|
+
the existing command. Start with decisions, lessons and roles, not arbitrary files.
|
|
52
|
+
|
|
53
|
+
Each domain adapter should describe these independent facts:
|
|
54
|
+
|
|
55
|
+
| Axis | Proposed values / rule |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| Canonical observation | `missing`, `valid`, `invalid_json`, `invalid_shape`, `unreadable`, `unsupported`, `unknown`; preserve the native domain reason code |
|
|
58
|
+
| Effective source | `canonical`, `legacy_markdown`, `none`; no silent switch of authority |
|
|
59
|
+
| Fallback | Explicit attempted/used state and reason; fallback availability does not make canonical data valid |
|
|
60
|
+
| Validation | Separate syntax, schema and item validation; unperformed checks are unknown, not true |
|
|
61
|
+
| Coverage | Enumerated stores, skipped stores and reason, bytes/limits; no “all healthy” from an empty findings list |
|
|
62
|
+
| Write effects | Zero for the diagnostic itself; not a promise about separate mutation commands |
|
|
63
|
+
|
|
64
|
+
These labels are design vocabulary, not existing wire fields. Public output should
|
|
65
|
+
contain paths/allowlisted codes and bounded metadata, not memory text, parse-error
|
|
66
|
+
excerpts, credentials, prompts or provider output. Missing optional data is not a
|
|
67
|
+
failure; inability to determine its state must not become success. Preserve existing
|
|
68
|
+
read fallback until an explicit compatibility change is implemented and tested.
|
|
69
|
+
|
|
70
|
+
Reuse domain validators and scope resolution. Avoid a universal schema or recursive
|
|
71
|
+
payload scan. A role-validator error must remain an error; an array-only legacy
|
|
72
|
+
memory read must not be advertised as complete item validation. Before parsing,
|
|
73
|
+
require bounded regular-file reads, appropriate link/identity checks and strict
|
|
74
|
+
decoding. Reuse pure domain schema validation on bounded input; do not assume the
|
|
75
|
+
current `readRoleStore` whole-file read enforces a physical read cap during growth.
|
|
76
|
+
A diagnostic snapshot is not a writer lease or a durable authority.
|
|
77
|
+
|
|
78
|
+
## Implementation order and acceptance gates
|
|
79
|
+
|
|
80
|
+
1. **Observability:** separate read-only diagnostics, preserving existing metadata-only
|
|
81
|
+
behavior. Check valid/absent/blank/syntax/shape/read-error/oversize inputs and
|
|
82
|
+
effective-source provenance; assert original bytes, file set and mtimes unchanged.
|
|
83
|
+
2. **Memory mutation admission:** validate the canonical source inside the operation's
|
|
84
|
+
lock and before archive, projection, log or canonical writes. Retest add, dedup,
|
|
85
|
+
drop and restore. Handle concurrent replacement and failed later writes explicitly;
|
|
86
|
+
an early check alone does not create a transaction.
|
|
87
|
+
3. **Migration B:** reuse the validated source-selection contract. Refuse uncertain
|
|
88
|
+
canonical authority, retain originals and evidence, and prove rollback and legacy
|
|
89
|
+
writer admission. Only then advance runtime activation.
|
|
90
|
+
|
|
91
|
+
Characterization checks of today's skip behavior document a gap; they are not an
|
|
92
|
+
acceptance requirement to preserve that gap forever. New diagnostics require new
|
|
93
|
+
tests for explicit unknown/error reporting. Existing role/runtime guards must not
|
|
94
|
+
be weakened, and a source inventory is not a complete writer audit.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Verification evidence: what a passing check means
|
|
2
|
+
|
|
3
|
+
This guide describes the existing single-task `verify-claim --json` contract. It does not introduce a new verifier or change exit codes. 한국어 요약은 아래에 있습니다.
|
|
4
|
+
|
|
5
|
+
## Read the result, not just the label
|
|
6
|
+
|
|
7
|
+
| Field | Meaning | Not a guarantee of |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `ok`, `reasons` | Overall result of the applicable claim checks | Correct requirements, complete coverage, production readiness |
|
|
10
|
+
| `semanticVerified: false` | Semantic correctness is not established by this verifier | A hidden semantic review having passed |
|
|
11
|
+
| `evidenceLevel: "static"` | No non-skipped test-run result was collected | Tests passing; absence of every other kind of execution |
|
|
12
|
+
| `evidenceLevel: "executed"` | A non-skipped test-run result was collected, including failed runs | A successful run |
|
|
13
|
+
| `run.skipped` | The selected test execution was skipped; inspect `reason` | A test failure or success |
|
|
14
|
+
| `run.allPassed` | Exit status was zero and, if parsed, the reporter's passed/total counts agree | Meaningful tests, sufficient coverage, or any particular number when `parsed` is null |
|
|
15
|
+
| `verdict.testCountMatch: null` | No applicable count comparison was made | A passing count comparison |
|
|
16
|
+
|
|
17
|
+
Without `--run-tests`, `run` is absent. With it, command selection is `--test-cmd`, then `.leerness/leerness-config.json`'s `testCommand`, then an available non-placeholder `npm test`, otherwise skip. A blocked command also skips. A skipped run stays `static` and does not itself add `tests-failed`; other checks can still fail.
|
|
18
|
+
|
|
19
|
+
An executed failure stays `executed` and contributes `tests-failed`. An exit-zero command with output the parser cannot recognize can have `parsed: null` and `allPassed: true`. Report that as successful command execution with unknown parsed test count, not “all requirements tested.” Only run commands from a workspace you trust: `--run-tests` executes project code.
|
|
20
|
+
|
|
21
|
+
These two boundary fields are present in the single-task JSON result, not promised for each `--all` aggregate entry. Static source checks count recognized declaration patterns; they do not run tests or establish coverage. Git checks compare working changes, not a reviewer's approval of a particular commit.
|
|
22
|
+
|
|
23
|
+
## A small executable example
|
|
24
|
+
|
|
25
|
+
Use a disposable directory outside your real project, initialize it with `leerness init . --minimal --yes`, and create these two files. The example assumes Node.js and an installed `leerness` CLI.
|
|
26
|
+
|
|
27
|
+
`sum.cjs`:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
module.exports = (a, b) => a + b;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`sum.test.cjs`:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const assert = require('node:assert/strict');
|
|
37
|
+
const sum = require('./sum.cjs');
|
|
38
|
+
assert.equal(sum(2, 3), 5);
|
|
39
|
+
console.log('1/1 passed');
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Register a task and substitute its actual printed ID for `T-0001` below. Record only what you actually ran; the first check deliberately makes no passing-test claim.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
leerness task add "Implement integer addition"
|
|
46
|
+
leerness task update T-0001 --status done --evidence "Files: sum.cjs, sum.test.cjs"
|
|
47
|
+
leerness verify-claim T-0001 --json
|
|
48
|
+
leerness verify-claim T-0001 --run-tests --test-cmd "node sum.test.cjs" --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The first result is `static`, with no `run`. The second is `executed`; for the supplied assertion, expect `run.exitCode: 0`, `run.allPassed: true`, and parsed count 1/1. Both retain `semanticVerified: false`. These are expected observations in the isolated example, not universal pass promises for an existing project's other evidence checks.
|
|
52
|
+
|
|
53
|
+
Now change the expected value from `5` to `6` in the disposable test and rerun the second command. The assertion fails: expect a nonzero CLI exit, `ok: false`, `executed`, and `tests-failed`. Restore `5` and rerun before recording success. Do not use a command that merely prints a pass count as proof of test quality.
|
|
54
|
+
|
|
55
|
+
## Keep an auditable handoff
|
|
56
|
+
|
|
57
|
+
Alongside the claim, record the project/worktree and commit tested, exact command, exit status, parsed count or “unavailable,” log location, and known exclusions. Keep failed and skipped attempts distinct from subsequent success. Review should identify the exact commit reviewed. These are reporting practices, not fields automatically guaranteed by every existing command.
|
|
58
|
+
|
|
59
|
+
For a sibling repository, run its tests there. Do not pretend its paths are relative to the current repository. A local evidence document can reference the external commit and logs, but verifying that document's existence does not rerun or validate the sibling implementation.
|
|
60
|
+
|
|
61
|
+
## 한국어 요약
|
|
62
|
+
|
|
63
|
+
- `ok`는 적용된 검사 결과이고 `executed`는 실행 여부의 분류입니다. 실패도 `executed`입니다.
|
|
64
|
+
- 미실행은 `run.skipped`와 사유를 확인합니다. `null`은 미측정/비적용이지 통과가 아닙니다.
|
|
65
|
+
- 종료 코드가 0이어도 `parsed: null`이면 테스트 개수는 미확인입니다. 요구사항 충족이나 테스트 품질을 증명하지 않습니다.
|
|
66
|
+
- 위 예제는 실제 assertion 성공/실패를 비교합니다. 정적 검사와 실행 결과를 섞지 말고 명령·종료 코드·검증한 commit·미검증 범위를 함께 기록합니다.
|
|
67
|
+
- `semanticVerified`는 계속 `false`이며, 이 가이드는 현재 계약을 설명할 뿐 새 의미론 검증이나 migration 활성화를 구현하지 않습니다.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { TextDecoder } = require('util');
|
|
6
|
+
const MAX_BYTES = 1024 * 1024;
|
|
7
|
+
const NAMES = new Set(['decisions.json', 'lessons.json', 'agent-roles.json', 'decisions.md', 'lessons.md']);
|
|
8
|
+
const identity = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode;
|
|
9
|
+
const snapshot = (a, b) => identity(a, b) && a.size === b.size
|
|
10
|
+
&& a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs && a.nlink === b.nlink;
|
|
11
|
+
const regular = stat => stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1n;
|
|
12
|
+
|
|
13
|
+
// 고정된 최상위 파일만 읽는다. 반환 text는 내부용이며 공개 보고서에 복사하지 않는다.
|
|
14
|
+
const readStoreFile = (workspace, name) => {
|
|
15
|
+
let fd = null;
|
|
16
|
+
let bytesRead = 0;
|
|
17
|
+
const fail = (status, reasonCode) => ({ status, reasonCode, bytesRead });
|
|
18
|
+
if (!NAMES.has(name)) return fail('unsupported', 'store_not_supported');
|
|
19
|
+
const file = path.join(workspace, name);
|
|
20
|
+
try {
|
|
21
|
+
let parent;
|
|
22
|
+
try { parent = fs.lstatSync(workspace, { bigint: true }); }
|
|
23
|
+
catch (error) { return fail(error.code === 'ENOENT' ? 'missing' : 'unreadable', 'workspace_unavailable'); }
|
|
24
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) return fail('unsupported', 'workspace_type');
|
|
25
|
+
const parentStable = () => {
|
|
26
|
+
const now = fs.lstatSync(workspace, { bigint: true });
|
|
27
|
+
return now.isDirectory() && !now.isSymbolicLink() && identity(parent, now);
|
|
28
|
+
};
|
|
29
|
+
let before;
|
|
30
|
+
try { before = fs.lstatSync(file, { bigint: true }); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!parentStable()) return fail('unknown', 'workspace_changed');
|
|
33
|
+
return fail(error.code === 'ENOENT' ? 'missing' : 'unreadable', error.code === 'ENOENT' ? 'file_missing' : 'stat_failed');
|
|
34
|
+
}
|
|
35
|
+
if (!regular(before)) return fail('unsupported', 'file_type_or_links');
|
|
36
|
+
if (before.size > BigInt(MAX_BYTES)) return fail('unsupported', 'size_limit');
|
|
37
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0);
|
|
38
|
+
fd = fs.openSync(file, flags);
|
|
39
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
40
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
41
|
+
if (!parentStable() || !regular(opened) || !regular(current)
|
|
42
|
+
|| !snapshot(before, opened) || !snapshot(opened, current)) return fail('unknown', 'changed_before_read');
|
|
43
|
+
// 크기 검사와 별개로 실제 I/O를 limit+1 바이트에서 중단한다.
|
|
44
|
+
const buffer = Buffer.alloc(MAX_BYTES + 1);
|
|
45
|
+
while (bytesRead < buffer.length) {
|
|
46
|
+
const n = fs.readSync(fd, buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
47
|
+
if (n === 0) break;
|
|
48
|
+
bytesRead += n;
|
|
49
|
+
}
|
|
50
|
+
if (bytesRead > MAX_BYTES) return fail('unsupported', 'size_limit');
|
|
51
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
52
|
+
const afterPath = fs.lstatSync(file, { bigint: true });
|
|
53
|
+
if (!parentStable() || !regular(afterPath) || !snapshot(opened, after)
|
|
54
|
+
|| !snapshot(after, afterPath) || BigInt(bytesRead) !== after.size) return fail('unknown', 'changed_during_read');
|
|
55
|
+
let text;
|
|
56
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, bytesRead)); }
|
|
57
|
+
catch { return fail('unsupported', 'invalid_utf8'); }
|
|
58
|
+
return { status: 'valid', reasonCode: 'read_ok', bytesRead, text };
|
|
59
|
+
} catch {
|
|
60
|
+
return fail('unreadable', 'read_failed');
|
|
61
|
+
} finally {
|
|
62
|
+
if (fd !== null) fs.closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
module.exports = { readStoreFile, MAX_BYTES };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveStatePaths } = require('./state-paths');
|
|
4
|
+
const { readStoreFile, MAX_BYTES } = require('./store-diagnostic-read');
|
|
5
|
+
const { validateLegacyRoleStore } = require('./role-agent-schema');
|
|
6
|
+
const { validateRoleDefinitionShape } = require('./role-fallback');
|
|
7
|
+
const { _decisionsFromMd, _parseLessonEntries } = require('./pure-utils');
|
|
8
|
+
|
|
9
|
+
const observeCanonical = (workspace, name, file) => {
|
|
10
|
+
const read = readStoreFile(workspace, file);
|
|
11
|
+
const result = { status: read.status, reasonCode: read.reasonCode, bytesRead: read.bytesRead,
|
|
12
|
+
syntaxValid: null, shapeValid: null, itemsValid: null };
|
|
13
|
+
if (read.status !== 'valid') return { canonical: result, count: null };
|
|
14
|
+
let document;
|
|
15
|
+
try { document = JSON.parse(read.text); result.syntaxValid = true; }
|
|
16
|
+
catch {
|
|
17
|
+
return { canonical: { ...result, status: 'invalid_json', reasonCode: 'json_syntax', syntaxValid: false }, count: null };
|
|
18
|
+
}
|
|
19
|
+
if (name !== 'roles') {
|
|
20
|
+
const valid = Array.isArray(document);
|
|
21
|
+
return { canonical: { ...result, shapeValid: valid,
|
|
22
|
+
status: valid ? 'valid' : 'invalid_shape', reasonCode: valid ? 'array_valid_items_unchecked' : 'array_required' },
|
|
23
|
+
count: valid ? document.length : null };
|
|
24
|
+
}
|
|
25
|
+
const shape = validateLegacyRoleStore(document);
|
|
26
|
+
const extended = shape.ok && Object.entries(document.roles).every(([role, value]) =>
|
|
27
|
+
validateRoleDefinitionShape(role, value, { allowLegacyProviderIds: true }).ok);
|
|
28
|
+
return { canonical: { ...result, shapeValid: shape.ok, itemsValid: shape.ok ? extended : null,
|
|
29
|
+
status: shape.ok && extended ? 'valid' : 'invalid_shape', reasonCode: shape.ok && extended ? 'role_schema_valid' : 'role_schema_invalid' },
|
|
30
|
+
count: shape.ok && extended ? Object.keys(document.roles).length : null };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const observeStore = (workspace, name, file) => {
|
|
34
|
+
const { canonical, count } = observeCanonical(workspace, name, file);
|
|
35
|
+
const row = { name, file, canonical, effectiveSource: canonical.status === 'valid' ? 'canonical' : 'none',
|
|
36
|
+
itemCount: count, fallback: { attempted: false, used: false, status: 'not_attempted' } };
|
|
37
|
+
if (name === 'roles' || !['missing', 'invalid_json', 'invalid_shape'].includes(canonical.status)) return row;
|
|
38
|
+
const fallback = readStoreFile(workspace, `${name}.md`);
|
|
39
|
+
row.fallback = { attempted: true, used: false, status: fallback.status,
|
|
40
|
+
reasonCode: fallback.reasonCode, bytesRead: fallback.bytesRead };
|
|
41
|
+
if (fallback.status !== 'valid') return row;
|
|
42
|
+
try {
|
|
43
|
+
const entries = (name === 'decisions' ? _decisionsFromMd : _parseLessonEntries)(fallback.text);
|
|
44
|
+
row.itemCount = entries.length;
|
|
45
|
+
row.effectiveSource = 'legacy_markdown';
|
|
46
|
+
row.fallback.used = true;
|
|
47
|
+
} catch {
|
|
48
|
+
row.fallback.status = 'unknown';
|
|
49
|
+
row.fallback.reasonCode = 'markdown_parse_failed';
|
|
50
|
+
}
|
|
51
|
+
return row;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const inspectStores = (root, options = {}) => {
|
|
55
|
+
const resolved = resolveStatePaths(root, options);
|
|
56
|
+
const workspace = resolved.workspace.selectedPath;
|
|
57
|
+
const stores = [['decisions', 'decisions.json'], ['lessons', 'lessons.json'], ['roles', 'agent-roles.json']]
|
|
58
|
+
.map(([name, file]) => observeStore(workspace, name, file));
|
|
59
|
+
const ok = stores.every(row => ['valid', 'missing'].includes(row.canonical.status)
|
|
60
|
+
&& ['not_attempted', 'valid', 'missing'].includes(row.fallback.status));
|
|
61
|
+
return { schema: 'leerness.store-diagnostics/v1', schemaVersion: 1, ok,
|
|
62
|
+
projectRoot: resolved.projectRoot, workspace, readOnly: true, writesPerformed: false,
|
|
63
|
+
runtimeActivated: false, migrationAvailable: false, maxBytesPerFile: MAX_BYTES,
|
|
64
|
+
coverage: { stores: stores.length, recursive: false, memoryItemsValidated: false },
|
|
65
|
+
warnings: resolved.warnings, stores };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const formatStoreDiagnostics = report => [
|
|
69
|
+
'Leerness store diagnostics (read-only; no migration)',
|
|
70
|
+
...report.stores.map(row => `${row.name}: ${row.canonical.status} (${row.canonical.reasonCode}); source=${row.effectiveSource}; fallback=${row.fallback.status}`),
|
|
71
|
+
'Memory arrays are not item/schema validation. A diagnostic is not writer admission.',
|
|
72
|
+
].join('\n');
|
|
73
|
+
|
|
74
|
+
module.exports = { inspectStores, formatStoreDiagnostics };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.188",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"test:role-agent-schema": "node ./scripts/role-agent-schema-probe.js",
|
|
71
71
|
"test:role-store-loader": "node ./scripts/role-store-loader-probe.js",
|
|
72
72
|
"test:role-fallback": "node ./scripts/role-fallback-probe.js",
|
|
73
|
-
"test:
|
|
73
|
+
"test:store-diagnostics": "node ./scripts/store-diagnostic-probe.js",
|
|
74
|
+
"test:state-scopes": "node ./scripts/workspace-selection-probe.js && node ./scripts/state-scopes-probe.js && node ./scripts/state-inspect-cli-probe.js && npm run test:store-diagnostics",
|
|
74
75
|
"test:runtime-compatibility": "node ./scripts/runtime-layout-probe.js && node ./scripts/runtime-admission-probe.js && node ./scripts/runtime-write-probe.js && node ./scripts/runtime-repl-probe.js && node ./scripts/runtime-git-write-probe.js && node ./scripts/runtime-replacement-probe.js && node ./scripts/encoding-selftest-probe.js && node ./scripts/selftest-cleanup-probe.js"
|
|
75
76
|
},
|
|
76
77
|
"publishConfig": {
|
|
@@ -72,7 +72,7 @@ const catalog = run(['commands', '--json']);
|
|
|
72
72
|
const catalogJson = json(catalog);
|
|
73
73
|
const catalogRows = catalogJson?.categories ? Object.values(catalogJson.categories).flat() : [];
|
|
74
74
|
check('commands --json returns the machine-readable catalog',
|
|
75
|
-
catalog.status === 0 && catalogJson?.totalCommands ===
|
|
75
|
+
catalog.status === 0 && catalogJson?.totalCommands === 103 && catalogJson?.categories && !catalog.stderr,
|
|
76
76
|
catalog);
|
|
77
77
|
check('commands totalCommands equals the category sum',
|
|
78
78
|
catalogJson?.totalCommands === catalogRows.length,
|
|
@@ -80,7 +80,8 @@ check('commands totalCommands equals the category sum',
|
|
|
80
80
|
check('commands catalog includes verify-code, contract verify, and exact-file lease',
|
|
81
81
|
catalogRows.some(row => /^verify-code\b/.test(row.cmd))
|
|
82
82
|
&& catalogRows.some(row => /^contract verify\b/.test(row.cmd))
|
|
83
|
-
&& catalogRows.some(row => /^lease acquire\|release\|list\|check\b/.test(row.cmd))
|
|
83
|
+
&& catalogRows.some(row => /^lease acquire\|release\|list\|check\b/.test(row.cmd))
|
|
84
|
+
&& catalogRows.some(row => /^state stores\b/.test(row.cmd)),
|
|
84
85
|
catalog);
|
|
85
86
|
|
|
86
87
|
const englishCatalog = run(['commands', '--language', 'en', '--json']);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Functional contract checks using only disposable, locally owned fixtures.
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const cp = require('child_process');
|
|
11
|
+
const vm = require('vm');
|
|
12
|
+
const { inspectStores } = require('../lib/store-diagnostics');
|
|
13
|
+
const { _shouldDrop } = require('../lib/git');
|
|
14
|
+
const CLI = path.resolve(__dirname, '../bin/leerness.js');
|
|
15
|
+
const tempRoot = fs.realpathSync.native(os.tmpdir());
|
|
16
|
+
const fixture = fs.mkdtempSync(path.join(tempRoot, 'leerness-store-diagnostic-'));
|
|
17
|
+
const fixtureIdentity = fs.realpathSync.native(fixture);
|
|
18
|
+
const LIMIT = 1024 * 1024;
|
|
19
|
+
const PRIVATE = 'synthetic-private-fixture-content';
|
|
20
|
+
let passed = 0;
|
|
21
|
+
|
|
22
|
+
function check(label, action) {
|
|
23
|
+
action();
|
|
24
|
+
passed++;
|
|
25
|
+
console.log(`PASS ${label}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function project(name, files = {}) {
|
|
29
|
+
const root = path.join(fixture, name);
|
|
30
|
+
fs.mkdirSync(path.join(root, '.leerness'), { recursive: true });
|
|
31
|
+
for (const [file, content] of Object.entries(files)) {
|
|
32
|
+
fs.writeFileSync(path.join(root, '.leerness', file), content);
|
|
33
|
+
}
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function snapshot(root) {
|
|
38
|
+
const rows = {};
|
|
39
|
+
function visit(file, relative) {
|
|
40
|
+
const stat = fs.lstatSync(file);
|
|
41
|
+
const row = { mtimeMs: stat.mtimeMs, kind: stat.isDirectory() ? 'dir' : 'file' };
|
|
42
|
+
if (stat.isFile()) row.sha256 = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
43
|
+
rows[relative] = row;
|
|
44
|
+
if (stat.isDirectory()) for (const name of fs.readdirSync(file).sort()) visit(path.join(file, name), `${relative}/${name}`);
|
|
45
|
+
}
|
|
46
|
+
visit(root, '.');
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function inspect(root) {
|
|
51
|
+
const before = snapshot(fixture);
|
|
52
|
+
const result = inspectStores(root);
|
|
53
|
+
assert.strictEqual(result.schema, 'leerness.store-diagnostics/v1');
|
|
54
|
+
assert.strictEqual(result.readOnly, true);
|
|
55
|
+
assert.strictEqual(result.writesPerformed, false);
|
|
56
|
+
assert.strictEqual(typeof result.ok, 'boolean');
|
|
57
|
+
assert.deepStrictEqual(result.stores.map(row => row.name).sort(), ['decisions', 'lessons', 'roles']);
|
|
58
|
+
assert.deepStrictEqual(snapshot(fixture), before, 'inspection must preserve file set, hashes and mtimes');
|
|
59
|
+
assert(!JSON.stringify(result).includes(PRIVATE), 'raw fixture content must not appear in diagnostics');
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function row(report, name) { return report.stores.find(store => store.name === name); }
|
|
64
|
+
const legacy = {
|
|
65
|
+
'decisions.md': `# Decisions\n\n### 2026-09-08 — Fixture\n- Decision: ${PRIVATE}\n- Reason: test\n`,
|
|
66
|
+
'lessons.md': `# Lessons\n\n### 2026-09-08\n- Lesson: ${PRIVATE}\n- Tag: fixture\n`,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
function fakeRead(mode) {
|
|
71
|
+
const workspace = path.join(fixture, 'in-memory-workspace');
|
|
72
|
+
const regular = { dev: 1n, ino: 2n, mode: 1n, size: 2n, nlink: 1n,
|
|
73
|
+
mtimeNs: 1n, ctimeNs: 1n, isFile: () => true, isSymbolicLink: () => false };
|
|
74
|
+
const parent = { dev: 1n, ino: 1n, mode: 2n, isDirectory: () => true, isSymbolicLink: () => false };
|
|
75
|
+
const metrics = { requested: 0, consumed: 0, closes: 0 };
|
|
76
|
+
const fakeFs = {
|
|
77
|
+
constants: fs.constants,
|
|
78
|
+
lstatSync: file => file === workspace ? parent : regular,
|
|
79
|
+
openSync: () => 9,
|
|
80
|
+
fstatSync: () => regular,
|
|
81
|
+
readSync(fd, buffer, offset, length) {
|
|
82
|
+
assert.strictEqual(fd, 9);
|
|
83
|
+
metrics.requested += length;
|
|
84
|
+
assert(metrics.requested <= LIMIT + 1, 'actual I/O requests must remain bounded');
|
|
85
|
+
if (mode === 'denied') throw Object.assign(new Error('synthetic read failure'), { code: 'EACCES' });
|
|
86
|
+
buffer.fill(0x20, offset, offset + length);
|
|
87
|
+
metrics.consumed += length;
|
|
88
|
+
return length;
|
|
89
|
+
},
|
|
90
|
+
closeSync(fd) { assert.strictEqual(fd, 9); metrics.closes++; },
|
|
91
|
+
};
|
|
92
|
+
const module = { exports: {} };
|
|
93
|
+
vm.runInNewContext(fs.readFileSync(path.resolve(__dirname, '../lib/store-diagnostic-read.js'), 'utf8'), {
|
|
94
|
+
module, Buffer, require: name => name === 'fs' ? fakeFs : require(name),
|
|
95
|
+
}, { filename: 'store-diagnostic-read.js', timeout: 2000 });
|
|
96
|
+
assert.strictEqual(module.exports.MAX_BYTES, LIMIT);
|
|
97
|
+
return { result: module.exports.readStoreFile(workspace, 'decisions.json'), metrics };
|
|
98
|
+
}
|
|
99
|
+
check('in-memory continuous reader stops at the actual byte limit despite small metadata', () => {
|
|
100
|
+
const { result, metrics } = fakeRead('continuous');
|
|
101
|
+
assert.strictEqual(result.status, 'unsupported');
|
|
102
|
+
assert.strictEqual(result.reasonCode, 'size_limit');
|
|
103
|
+
assert.strictEqual(result.bytesRead, LIMIT + 1);
|
|
104
|
+
assert.strictEqual(metrics.requested, LIMIT + 1);
|
|
105
|
+
assert.strictEqual(metrics.consumed, LIMIT + 1);
|
|
106
|
+
assert.strictEqual(metrics.closes, 1);
|
|
107
|
+
});
|
|
108
|
+
check('in-memory read failure is unreadable and closes its descriptor once', () => {
|
|
109
|
+
const { result, metrics } = fakeRead('denied');
|
|
110
|
+
assert.strictEqual(result.status, 'unreadable');
|
|
111
|
+
assert.strictEqual(result.reasonCode, 'read_failed');
|
|
112
|
+
assert.strictEqual(result.bytesRead, 0);
|
|
113
|
+
assert.strictEqual(metrics.consumed, 0);
|
|
114
|
+
assert.strictEqual(metrics.closes, 1);
|
|
115
|
+
});
|
|
116
|
+
check('absent optional stores are missing, not invented empty canonical arrays', () => {
|
|
117
|
+
const report = inspect(project('absent'));
|
|
118
|
+
assert.strictEqual(report.ok, true);
|
|
119
|
+
for (const store of report.stores) {
|
|
120
|
+
assert.strictEqual(store.canonical.status, 'missing');
|
|
121
|
+
assert.strictEqual(store.effectiveSource, 'none');
|
|
122
|
+
assert.strictEqual(store.itemCount, null);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const valid = project('valid target 한글', {
|
|
126
|
+
'decisions.json': JSON.stringify([{ title: PRIVATE }]),
|
|
127
|
+
'lessons.json': '[]',
|
|
128
|
+
'agent-roles.json': JSON.stringify({ schemaVersion: 1, roles: { implementer: { provider: 'codex', model: 'fixture-model' } } }),
|
|
129
|
+
...legacy,
|
|
130
|
+
});
|
|
131
|
+
check('valid canonical stores take precedence without reading fallback', () => {
|
|
132
|
+
const report = inspect(valid);
|
|
133
|
+
assert.strictEqual(report.ok, true);
|
|
134
|
+
for (const store of report.stores) {
|
|
135
|
+
assert.strictEqual(store.canonical.status, 'valid');
|
|
136
|
+
assert.strictEqual(store.canonical.syntaxValid, true);
|
|
137
|
+
assert.strictEqual(store.canonical.shapeValid, true);
|
|
138
|
+
assert.strictEqual(store.effectiveSource, 'canonical');
|
|
139
|
+
assert.strictEqual(store.fallback.attempted, false);
|
|
140
|
+
assert.strictEqual(store.fallback.used, false);
|
|
141
|
+
assert(store.canonical.bytesRead > 0);
|
|
142
|
+
}
|
|
143
|
+
assert.strictEqual(row(report, 'decisions').itemCount, 1);
|
|
144
|
+
assert.strictEqual(row(report, 'lessons').itemCount, 0);
|
|
145
|
+
assert.strictEqual(row(report, 'roles').itemCount, 1);
|
|
146
|
+
assert.strictEqual(row(report, 'decisions').canonical.itemsValid, null);
|
|
147
|
+
assert.strictEqual(row(report, 'lessons').canonical.itemsValid, null);
|
|
148
|
+
assert.strictEqual(row(report, 'roles').canonical.itemsValid, true);
|
|
149
|
+
});
|
|
150
|
+
check('memory array shape acceptance does not claim item validation', () => {
|
|
151
|
+
const report = inspect(project('array shape only', { 'decisions.json': '[null,1,"fixture"]' }));
|
|
152
|
+
assert.strictEqual(report.ok, true);
|
|
153
|
+
assert.strictEqual(row(report, 'decisions').canonical.itemsValid, null);
|
|
154
|
+
assert.strictEqual(row(report, 'decisions').itemCount, 3);
|
|
155
|
+
});
|
|
156
|
+
for (const [label, content, status, fallbackAllowed] of [
|
|
157
|
+
['invalid JSON', '{', 'invalid_json', true],
|
|
158
|
+
['object shape', '{}', 'invalid_shape', true],
|
|
159
|
+
['blank', ' \n', 'invalid_json', true],
|
|
160
|
+
['oversize', ' '.repeat(LIMIT + 100), 'unsupported', false],
|
|
161
|
+
['invalid UTF8', Buffer.from([0xc3, 0x28]), 'unsupported', false],
|
|
162
|
+
]) {
|
|
163
|
+
check(`${label}: explicit canonical failure remains visible with legacy files present`, () => {
|
|
164
|
+
const report = inspect(project(label, { 'decisions.json': content, 'lessons.json': content, ...legacy }));
|
|
165
|
+
assert.strictEqual(report.ok, false);
|
|
166
|
+
for (const name of ['decisions', 'lessons']) {
|
|
167
|
+
const store = row(report, name);
|
|
168
|
+
assert.strictEqual(store.canonical.status, status);
|
|
169
|
+
assert.strictEqual(typeof store.canonical.reasonCode, 'string');
|
|
170
|
+
assert(store.canonical.bytesRead <= LIMIT + 1);
|
|
171
|
+
assert.strictEqual(store.fallback.attempted, fallbackAllowed);
|
|
172
|
+
assert.strictEqual(store.fallback.used, fallbackAllowed);
|
|
173
|
+
assert.strictEqual(store.effectiveSource, fallbackAllowed ? 'legacy_markdown' : 'none');
|
|
174
|
+
assert.strictEqual(store.itemCount, fallbackAllowed ? 1 : null);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
check('missing canonical files can report legacy counts without backfill', () => {
|
|
179
|
+
const report = inspect(project('legacy only', legacy));
|
|
180
|
+
assert.strictEqual(report.ok, true);
|
|
181
|
+
for (const name of ['decisions', 'lessons']) {
|
|
182
|
+
const store = row(report, name);
|
|
183
|
+
assert.strictEqual(store.canonical.status, 'missing');
|
|
184
|
+
assert.strictEqual(store.fallback.status, 'valid');
|
|
185
|
+
assert.strictEqual(store.fallback.used, true);
|
|
186
|
+
assert.strictEqual(store.itemCount, 1);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
check('invalid role definitions are not accepted as valid assignments', () => {
|
|
190
|
+
const report = inspect(project('invalid roles', { 'agent-roles.json': JSON.stringify({ schemaVersion: 1, roles: { reviewer: { provider: 42 } } }) }));
|
|
191
|
+
assert.strictEqual(report.ok, false);
|
|
192
|
+
const store = row(report, 'roles');
|
|
193
|
+
assert.strictEqual(store.canonical.status, 'invalid_shape');
|
|
194
|
+
assert.strictEqual(store.canonical.syntaxValid, true);
|
|
195
|
+
assert.strictEqual(store.fallback.attempted, false);
|
|
196
|
+
assert.strictEqual(store.effectiveSource, 'none');
|
|
197
|
+
assert.strictEqual(store.itemCount, null);
|
|
198
|
+
});
|
|
199
|
+
check('invalid role schema versions remain explicit failures', () => {
|
|
200
|
+
const report = inspect(project('role schema', { 'agent-roles.json': '{"schemaVersion":999,"roles":{}}' }));
|
|
201
|
+
assert.strictEqual(report.ok, false);
|
|
202
|
+
assert.strictEqual(row(report, 'roles').canonical.status, 'invalid_shape');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const cwd = project('unrelated cwd', { 'current-state.md': 'Keep user state.\n', 'manifest.json': '{"language":"ko"}' });
|
|
206
|
+
const env = { ...process.env };
|
|
207
|
+
for (const key of Object.keys(env)) if (/^(LEERNESS_|GIT_CONFIG_|NODE_OPTIONS)/i.test(key) || _shouldDrop(key)) delete env[key];
|
|
208
|
+
env.LEERNESS_LANG = 'en';
|
|
209
|
+
function cli(target, extra = []) {
|
|
210
|
+
return cp.spawnSync(process.execPath, [CLI, 'state', 'stores', target, ...extra, '--json'], { cwd, env, encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024 });
|
|
211
|
+
}
|
|
212
|
+
check('real CLI from unrelated cwd preserves target and cwd file sets, hashes and mtimes', () => {
|
|
213
|
+
const before = snapshot(fixture);
|
|
214
|
+
const result = cli(valid);
|
|
215
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
216
|
+
assert.strictEqual(result.stderr, '');
|
|
217
|
+
const report = JSON.parse(result.stdout);
|
|
218
|
+
assert.strictEqual(report.schema, 'leerness.store-diagnostics/v1');
|
|
219
|
+
assert.strictEqual(report.ok, true);
|
|
220
|
+
assert.strictEqual(report.readOnly, true);
|
|
221
|
+
assert.strictEqual(report.writesPerformed, false);
|
|
222
|
+
assert.strictEqual(row(report, 'decisions').itemCount, 1);
|
|
223
|
+
assert(!result.stdout.includes(PRIVATE));
|
|
224
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
225
|
+
});
|
|
226
|
+
check('real CLI failed diagnostic keeps canonical failure despite successful fallback and writes nothing', () => {
|
|
227
|
+
const target = project('cli invalid', { 'decisions.json': '{', ...legacy });
|
|
228
|
+
const before = snapshot(fixture);
|
|
229
|
+
const result = cli(target);
|
|
230
|
+
assert.strictEqual(result.status, 1, result.stderr || result.stdout);
|
|
231
|
+
assert.strictEqual(result.stderr, '');
|
|
232
|
+
const report = JSON.parse(result.stdout);
|
|
233
|
+
assert.strictEqual(report.ok, false);
|
|
234
|
+
assert.strictEqual(row(report, 'decisions').canonical.status, 'invalid_json');
|
|
235
|
+
assert.strictEqual(row(report, 'decisions').fallback.used, true);
|
|
236
|
+
assert(!result.stdout.includes(PRIVATE));
|
|
237
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
238
|
+
});
|
|
239
|
+
for (const [label, args] of [
|
|
240
|
+
['mutation flag', ['--apply']],
|
|
241
|
+
['extra positional path', [cwd]],
|
|
242
|
+
['duplicate explicit path', ['--path', valid, '--path', cwd]],
|
|
243
|
+
]) {
|
|
244
|
+
check(`real CLI rejects ${label} without writes`, () => {
|
|
245
|
+
const before = snapshot(fixture);
|
|
246
|
+
const result = cli(valid, args);
|
|
247
|
+
assert.strictEqual(result.status, 1, result.stderr || result.stdout);
|
|
248
|
+
assert.strictEqual(result.stderr, '');
|
|
249
|
+
assert.strictEqual(JSON.parse(result.stdout).ok, false);
|
|
250
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
check('explicit --path overrides an unrelated positional root without writes', () => {
|
|
254
|
+
const before = snapshot(fixture);
|
|
255
|
+
const result = cli(cwd, ['--path', valid]);
|
|
256
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
257
|
+
assert.strictEqual(result.stderr, '');
|
|
258
|
+
const report = JSON.parse(result.stdout);
|
|
259
|
+
assert.strictEqual(report.ok, true);
|
|
260
|
+
assert.strictEqual(report.projectRoot, fs.realpathSync.native(valid));
|
|
261
|
+
assert.strictEqual(row(report, 'decisions').itemCount, 1);
|
|
262
|
+
assert.deepStrictEqual(snapshot(fixture), before);
|
|
263
|
+
});
|
|
264
|
+
console.log(`store-diagnostic probe: ${passed}/${passed} PASS`);
|
|
265
|
+
} finally {
|
|
266
|
+
// Resolve and check the exact allocated directory before recursive removal.
|
|
267
|
+
const resolved = fs.realpathSync.native(fixture);
|
|
268
|
+
assert.strictEqual(resolved, fixtureIdentity);
|
|
269
|
+
assert.strictEqual(path.dirname(resolved), tempRoot);
|
|
270
|
+
assert(path.basename(resolved).startsWith('leerness-store-diagnostic-'));
|
|
271
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
272
|
+
}
|