peaks-cli 1.3.7 → 1.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/core-artifact-commands.js +119 -14
- package/dist/src/cli/commands/project-commands.js +58 -1
- package/dist/src/cli/commands/request-commands.js +124 -4
- package/dist/src/cli/commands/retrospective-commands.d.ts +3 -0
- package/dist/src/cli/commands/retrospective-commands.js +113 -0
- package/dist/src/cli/program.js +2 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +16 -0
- package/dist/src/services/artifacts/request-artifact-service.js +18 -2
- package/dist/src/services/memory/project-memory-service.d.ts +19 -0
- package/dist/src/services/memory/project-memory-service.js +33 -0
- package/dist/src/services/retrospective/migrate-from-md.d.ts +37 -0
- package/dist/src/services/retrospective/migrate-from-md.js +528 -0
- package/dist/src/services/retrospective/retrospective-index.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-index.js +110 -0
- package/dist/src/services/retrospective/retrospective-show.d.ts +40 -0
- package/dist/src/services/retrospective/retrospective-show.js +109 -0
- package/dist/src/services/session/caller-binding-service.d.ts +70 -0
- package/dist/src/services/session/caller-binding-service.js +148 -0
- package/dist/src/services/session/caller-id-types.d.ts +77 -0
- package/dist/src/services/session/caller-id-types.js +46 -0
- package/dist/src/services/session/index.d.ts +4 -0
- package/dist/src/services/session/index.js +5 -0
- package/dist/src/services/session/platform-fallbacks.d.ts +31 -0
- package/dist/src/services/session/platform-fallbacks.js +35 -0
- package/dist/src/services/session/resolve-caller-id.d.ts +57 -0
- package/dist/src/services/session/resolve-caller-id.js +88 -0
- package/dist/src/services/skills/skill-presence-service.d.ts +11 -0
- package/dist/src/services/skills/skill-presence-service.js +59 -0
- package/dist/src/shared/format-md-compact.d.ts +32 -0
- package/dist/src/shared/format-md-compact.js +297 -0
- package/dist/src/shared/stale-policy.d.ts +67 -0
- package/dist/src/shared/stale-policy.js +85 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-qa/SKILL.md +86 -515
- package/skills/peaks-qa/references/artifact-per-request.md +7 -79
- package/skills/peaks-qa/references/browser-validation-contracts.md +51 -0
- package/skills/peaks-qa/references/codegraph-regression-focus.md +5 -0
- package/skills/peaks-qa/references/external-capability-guidance.md +9 -0
- package/skills/peaks-qa/references/qa-compact-handoff.md +3 -0
- package/skills/peaks-qa/references/qa-context-governance.md +24 -0
- package/skills/peaks-qa/references/qa-fanout-contract.md +8 -0
- package/skills/peaks-qa/references/qa-gstack-integration.md +7 -0
- package/skills/peaks-qa/references/qa-local-artifacts.md +3 -0
- package/skills/peaks-qa/references/qa-matt-pocock-integration.md +9 -0
- package/skills/peaks-qa/references/qa-refactor-role.md +3 -0
- package/skills/peaks-qa/references/qa-runbook.md +74 -0
- package/skills/peaks-qa/references/qa-skill-presence.md +22 -0
- package/skills/peaks-qa/references/qa-standards-preflight.md +8 -0
- package/skills/peaks-qa/references/qa-sub-agent-dispatch.md +38 -0
- package/skills/peaks-qa/references/qa-transition-gates.md +79 -0
- package/skills/peaks-qa/references/requirement-boundary-recheck.md +9 -0
- package/skills/peaks-qa/references/test-case-generation.md +27 -0
- package/skills/peaks-qa/references/test-report-output.md +14 -0
- package/skills/peaks-rd/SKILL.md +85 -732
- package/skills/peaks-rd/references/artifact-and-standards-output.md +9 -0
- package/skills/peaks-rd/references/artifact-per-request.md +20 -0
- package/skills/peaks-rd/references/browser-self-test-contracts.md +29 -0
- package/skills/peaks-rd/references/codegraph-project-analysis.md +5 -0
- package/skills/peaks-rd/references/compact-handoff.md +3 -0
- package/skills/peaks-rd/references/external-references.md +11 -0
- package/skills/peaks-rd/references/frontend-project-generation.md +11 -0
- package/skills/peaks-rd/references/library-version-awareness.md +30 -0
- package/skills/peaks-rd/references/mandatory-perf-baseline.md +40 -0
- package/skills/peaks-rd/references/mandatory-tech-doc.md +18 -0
- package/skills/peaks-rd/references/matt-pocock-integration.md +11 -0
- package/skills/peaks-rd/references/mock-data-placement.md +40 -0
- package/skills/peaks-rd/references/parallel-review-fanout.md +81 -0
- package/skills/peaks-rd/references/rd-context-governance.md +36 -0
- package/skills/peaks-rd/references/rd-gstack-integration.md +16 -0
- package/skills/peaks-rd/references/rd-runbook.md +125 -0
- package/skills/peaks-rd/references/rd-standards-preflight.md +8 -0
- package/skills/peaks-rd/references/rd-sub-agent-dispatch.md +39 -0
- package/skills/peaks-rd/references/rd-transition-gates.md +148 -0
- package/skills/peaks-rd/references/skill-presence-and-title.md +22 -0
- package/skills/peaks-solo/SKILL.md +87 -786
- package/skills/peaks-solo/references/anchoring-and-session-info.md +25 -0
- package/skills/peaks-solo/references/boundaries.md +21 -0
- package/skills/peaks-solo/references/codegraph-orchestration.md +5 -0
- package/skills/peaks-solo/references/completion-handoff.md +16 -0
- package/skills/peaks-solo/references/context-governance.md +51 -0
- package/skills/peaks-solo/references/external-references.md +17 -0
- package/skills/peaks-solo/references/frontend-only-mode.md +87 -0
- package/skills/peaks-solo/references/gstack-integration.md +7 -0
- package/skills/peaks-solo/references/local-artifact-workspace.md +79 -0
- package/skills/peaks-solo/references/micro-cycle.md +68 -0
- package/skills/peaks-solo/references/mode-selection.md +21 -0
- package/skills/peaks-solo/references/openspec-workflow.md +43 -0
- package/skills/peaks-solo/references/project-memory-loading.md +17 -0
- package/skills/peaks-solo/references/project-scan-checklist.md +136 -0
- package/skills/peaks-solo/references/quality-gate-cheatsheet.md +13 -0
- package/skills/peaks-solo/references/resume-detection.md +63 -0
- package/skills/peaks-solo/references/runbook.md +1 -1
- package/skills/peaks-solo/references/skill-presence-and-title.md +31 -0
- package/skills/peaks-solo/references/standards-preflight.md +23 -0
- package/skills/peaks-solo/references/sub-agent-dispatch.md +46 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +56 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLATFORM_FALLBACKS — the Level 3 fallback table for caller-id resolution.
|
|
3
|
+
*
|
|
4
|
+
* Slice 020 (D3): when neither `--caller-id` nor `PEAKS_CALLER_ID` is
|
|
5
|
+
* set, the resolver walks this table top-to-bottom and takes the
|
|
6
|
+
* first non-empty entry. Today there is exactly one entry: Claude
|
|
7
|
+
* Code (`CLAUDE_CODE_SESSION_ID`).
|
|
8
|
+
*
|
|
9
|
+
* To add a new platform (Cursor, Windsurf, peaks-ide, etc.):
|
|
10
|
+
*
|
|
11
|
+
* 1. Add a new entry below.
|
|
12
|
+
* 2. Bump the contract doc's A5 acceptance criterion
|
|
13
|
+
* (`.peaks/_runtime/2026-06-09-session-8bfe7d/prd/source/caller-id-contract.md`).
|
|
14
|
+
* 3. Add a regression test that asserts the new entry resolves
|
|
15
|
+
* correctly under D4 priority.
|
|
16
|
+
*
|
|
17
|
+
* The contract's A5 test (`tests/unit/services/session/caller-id-resolution.test.ts`)
|
|
18
|
+
* asserts `PLATFORM_FALLBACKS.length === 1`; adding a new entry will
|
|
19
|
+
* fail that test, forcing the contract bump.
|
|
20
|
+
*
|
|
21
|
+
* Adding an entry does NOT require code changes to read points
|
|
22
|
+
* (statusline, doctor, sc, session-info) — they all call the same
|
|
23
|
+
* resolver. Each entry is a one-line additive change.
|
|
24
|
+
*/
|
|
25
|
+
export const PLATFORM_FALLBACKS = [
|
|
26
|
+
{
|
|
27
|
+
envVar: 'CLAUDE_CODE_SESSION_ID',
|
|
28
|
+
description: 'Claude Code session id',
|
|
29
|
+
addedIn: '1.3.7'
|
|
30
|
+
}
|
|
31
|
+
// Future entries (do NOT add without bumping the contract's A5):
|
|
32
|
+
// { envVar: 'CURSOR_SESSION_ID', description: 'Cursor session id', addedIn: 'TBD' },
|
|
33
|
+
// { envVar: 'WINDSURF_SESSION_ID', description: 'Windsurf session id', addedIn: 'TBD' },
|
|
34
|
+
// { envVar: 'PEAKS_IDE_SESSION_ID', description: 'peaks-ide session id', addedIn: 'TBD' },
|
|
35
|
+
];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller-Id Resolution (slice 020 — caller-keyed session binding).
|
|
3
|
+
*
|
|
4
|
+
* `resolveCallerId` is the single source of truth for "who is calling
|
|
5
|
+
* the CLI". The resolver applies D4 priority (flag > env > platform
|
|
6
|
+
* fallback > reject) and validates the winner against D1's regex;
|
|
7
|
+
* failures throw `CallerIdError` (D2 → exit 64, D5 → exit 65).
|
|
8
|
+
*
|
|
9
|
+
* The function is synchronous and pure. It does NOT touch the
|
|
10
|
+
* filesystem, does NOT read any caller binding file, and does NOT
|
|
11
|
+
* mutate state. The caller (a CLI command, a service, a test) decides
|
|
12
|
+
* what to do with the resolved id.
|
|
13
|
+
*
|
|
14
|
+
* See `.peaks/_runtime/2026-06-09-session-8bfe7d/prd/source/caller-id-contract.md`
|
|
15
|
+
* for the freeze-in contract (D1-D7).
|
|
16
|
+
*/
|
|
17
|
+
import { CallerIdError } from './caller-id-types.js';
|
|
18
|
+
export { CallerIdError };
|
|
19
|
+
export interface ResolveCallerIdOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The `--caller-id <id>` flag value (per-invocation override).
|
|
22
|
+
* D4 priority level 1: flag wins.
|
|
23
|
+
*/
|
|
24
|
+
flagValue?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Override for the `PEAKS_CALLER_ID` environment variable. D4
|
|
27
|
+
* priority level 2: env wins. Defaults to `process.env.PEAKS_CALLER_ID`.
|
|
28
|
+
* The override exists so tests can run without mutating process.env.
|
|
29
|
+
*/
|
|
30
|
+
envOverride?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The env object to read. Defaults to `process.env`. Exists so
|
|
33
|
+
* tests can drive Level 3 (platform fallback) without mutating
|
|
34
|
+
* process.env.
|
|
35
|
+
*/
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the calling process's callerId per D1-D5.
|
|
40
|
+
*
|
|
41
|
+
* D4 priority (strict, no merge):
|
|
42
|
+
* 1. `opts.flagValue` (per-invocation `--caller-id <id>` override)
|
|
43
|
+
* 2. `opts.envOverride ?? process.env.PEAKS_CALLER_ID` (per-process declaration)
|
|
44
|
+
* 3. First non-empty entry in `PLATFORM_FALLBACKS` (platform default)
|
|
45
|
+
* 4. → **D2 fires**: throw `CallerIdError` (EX_USAGE, exit 64)
|
|
46
|
+
*
|
|
47
|
+
* On success: returns the resolved id (matches D1's regex, validated).
|
|
48
|
+
* On D2 (nothing set): throws `CallerIdError` (EX_USAGE, exit 64).
|
|
49
|
+
* On D5 (regex fail): throws `CallerIdError` (EX_DATAERR, exit 65).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* resolveCallerId({ flagValue: 'foo-bar' }) // → 'foo-bar'
|
|
53
|
+
* resolveCallerId({ envOverride: 'baz' }) // → 'baz'
|
|
54
|
+
* resolveCallerId({ env: { CLAUDE_CODE_SESSION_ID: 'sid-123' } }) // → 'sid-123'
|
|
55
|
+
* resolveCallerId() // → throws CallerIdError (EX_USAGE)
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveCallerId(opts?: ResolveCallerIdOptions): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller-Id Resolution (slice 020 — caller-keyed session binding).
|
|
3
|
+
*
|
|
4
|
+
* `resolveCallerId` is the single source of truth for "who is calling
|
|
5
|
+
* the CLI". The resolver applies D4 priority (flag > env > platform
|
|
6
|
+
* fallback > reject) and validates the winner against D1's regex;
|
|
7
|
+
* failures throw `CallerIdError` (D2 → exit 64, D5 → exit 65).
|
|
8
|
+
*
|
|
9
|
+
* The function is synchronous and pure. It does NOT touch the
|
|
10
|
+
* filesystem, does NOT read any caller binding file, and does NOT
|
|
11
|
+
* mutate state. The caller (a CLI command, a service, a test) decides
|
|
12
|
+
* what to do with the resolved id.
|
|
13
|
+
*
|
|
14
|
+
* See `.peaks/_runtime/2026-06-09-session-8bfe7d/prd/source/caller-id-contract.md`
|
|
15
|
+
* for the freeze-in contract (D1-D7).
|
|
16
|
+
*/
|
|
17
|
+
import { CALLER_ID_REGEX, CallerIdError } from './caller-id-types.js';
|
|
18
|
+
import { PLATFORM_FALLBACKS } from './platform-fallbacks.js';
|
|
19
|
+
// Re-export for CLI consumers (avoids a second import line).
|
|
20
|
+
export { CallerIdError };
|
|
21
|
+
/**
|
|
22
|
+
* Check whether `value` looks like a callerId (non-empty, matches D1).
|
|
23
|
+
* Returns the trimmed value if so, undefined otherwise. Does not throw.
|
|
24
|
+
*/
|
|
25
|
+
function isNonEmpty(value) {
|
|
26
|
+
return typeof value === 'string' && value.length > 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read a single platform-fallback env var from `env`. Returns the
|
|
30
|
+
* non-empty trimmed value or `undefined`. Logs nothing.
|
|
31
|
+
*/
|
|
32
|
+
function readPlatformFallback(env) {
|
|
33
|
+
for (let i = 0; i < PLATFORM_FALLBACKS.length; i++) {
|
|
34
|
+
const candidate = env[PLATFORM_FALLBACKS[i].envVar];
|
|
35
|
+
if (isNonEmpty(candidate)) {
|
|
36
|
+
return { value: candidate, index: i };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate `value` against D1's regex. Returns the value on success,
|
|
43
|
+
* throws `CallerIdError` (EX_DATAERR, exit 65) on failure.
|
|
44
|
+
*/
|
|
45
|
+
function validateCallerId(value, source) {
|
|
46
|
+
if (!CALLER_ID_REGEX.test(value)) {
|
|
47
|
+
throw new CallerIdError('EX_DATAERR', source, `Invalid caller id "${value}" (source: ${source}). callerId must match ^[a-zA-Z0-9._-]{1,200}$.`, value);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the calling process's callerId per D1-D5.
|
|
53
|
+
*
|
|
54
|
+
* D4 priority (strict, no merge):
|
|
55
|
+
* 1. `opts.flagValue` (per-invocation `--caller-id <id>` override)
|
|
56
|
+
* 2. `opts.envOverride ?? process.env.PEAKS_CALLER_ID` (per-process declaration)
|
|
57
|
+
* 3. First non-empty entry in `PLATFORM_FALLBACKS` (platform default)
|
|
58
|
+
* 4. → **D2 fires**: throw `CallerIdError` (EX_USAGE, exit 64)
|
|
59
|
+
*
|
|
60
|
+
* On success: returns the resolved id (matches D1's regex, validated).
|
|
61
|
+
* On D2 (nothing set): throws `CallerIdError` (EX_USAGE, exit 64).
|
|
62
|
+
* On D5 (regex fail): throws `CallerIdError` (EX_DATAERR, exit 65).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* resolveCallerId({ flagValue: 'foo-bar' }) // → 'foo-bar'
|
|
66
|
+
* resolveCallerId({ envOverride: 'baz' }) // → 'baz'
|
|
67
|
+
* resolveCallerId({ env: { CLAUDE_CODE_SESSION_ID: 'sid-123' } }) // → 'sid-123'
|
|
68
|
+
* resolveCallerId() // → throws CallerIdError (EX_USAGE)
|
|
69
|
+
*/
|
|
70
|
+
export function resolveCallerId(opts = {}) {
|
|
71
|
+
const env = opts.env ?? process.env;
|
|
72
|
+
// D4 level 1: flag value
|
|
73
|
+
if (isNonEmpty(opts.flagValue)) {
|
|
74
|
+
return validateCallerId(opts.flagValue, 'flag');
|
|
75
|
+
}
|
|
76
|
+
// D4 level 2: env var
|
|
77
|
+
const envValue = isNonEmpty(opts.envOverride) ? opts.envOverride : env.PEAKS_CALLER_ID;
|
|
78
|
+
if (isNonEmpty(envValue)) {
|
|
79
|
+
return validateCallerId(envValue, 'env');
|
|
80
|
+
}
|
|
81
|
+
// D4 level 3: PLATFORM_FALLBACKS table (top-to-bottom)
|
|
82
|
+
const fallback = readPlatformFallback(env);
|
|
83
|
+
if (fallback !== undefined) {
|
|
84
|
+
return validateCallerId(fallback.value, 'fallback');
|
|
85
|
+
}
|
|
86
|
+
// D4 level 4: D2 fires — no callerId available
|
|
87
|
+
throw new CallerIdError('EX_USAGE', 'none', 'No caller id available. Set PEAKS_CALLER_ID or pass --caller-id.');
|
|
88
|
+
}
|
|
@@ -43,6 +43,17 @@ export type SkillPresence = {
|
|
|
43
43
|
lastHeartbeat?: string;
|
|
44
44
|
};
|
|
45
45
|
export declare function exportSkillPresence(projectRootOverride?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Write the per-caller active-skill marker to
|
|
48
|
+
* `.peaks/_runtime/<peakSid>/active-skill-<callerId>.json` (D6). Returns
|
|
49
|
+
* the written presence with the `callerId` field set.
|
|
50
|
+
*
|
|
51
|
+
* The caller is responsible for resolving the `callerId` (via
|
|
52
|
+
* `resolveCallerId` from `src/services/session/resolve-caller-id.ts`)
|
|
53
|
+
* and the `peakSessionId` (via `getCallerBinding` then reading
|
|
54
|
+
* `peakSessionId`, OR via `ensureSession` for the first-time case).
|
|
55
|
+
*/
|
|
56
|
+
export declare function setSkillPresenceForCaller(projectRootOverride: string, callerId: string, peakSessionId: string, skill: string, mode?: string, gate?: string): SkillPresence;
|
|
46
57
|
export declare function setSkillPresence(skill: string, mode?: string, gate?: string, projectRootOverride?: string): SkillPresence;
|
|
47
58
|
export declare function getSkillPresence(projectRootOverride?: string): SkillPresence | null;
|
|
48
59
|
export declare function touchSkillHeartbeat(projectRootOverride?: string): SkillPresence | null;
|
|
@@ -160,6 +160,65 @@ function getPreviousOuterSessionId(projectRootOverride) {
|
|
|
160
160
|
export function exportSkillPresence(projectRootOverride) {
|
|
161
161
|
return resolvePresencePath(projectRootOverride);
|
|
162
162
|
}
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Slice 020 — caller-keyed active-skill marker (D6).
|
|
165
|
+
// ============================================================================
|
|
166
|
+
//
|
|
167
|
+
// Today's per-project active-skill marker (`.peaks/_runtime/active-skill.json`)
|
|
168
|
+
// races when multiple Claude Code windows (or different platforms) drive the
|
|
169
|
+
// same project concurrently. Slice 020 introduces a per-caller file at
|
|
170
|
+
// `.peaks/_runtime/<peakSid>/active-skill-<callerId>.json` (D6). Two callers
|
|
171
|
+
// bound to the same peak session never clobber each other.
|
|
172
|
+
//
|
|
173
|
+
// The single-file marker is RETAINED for one minor release as read-only
|
|
174
|
+
// back-compat (M1, M4). The new write path is `setSkillPresenceForCaller`;
|
|
175
|
+
// the legacy `setSkillPresence` is now a thin wrapper that synthesises a
|
|
176
|
+
// legacy callerId from `process.env.CLAUDE_CODE_SESSION_ID` (or
|
|
177
|
+
// `projectRoot` for the truly-anonymous case) and delegates.
|
|
178
|
+
/**
|
|
179
|
+
* Write the per-caller active-skill marker to
|
|
180
|
+
* `.peaks/_runtime/<peakSid>/active-skill-<callerId>.json` (D6). Returns
|
|
181
|
+
* the written presence with the `callerId` field set.
|
|
182
|
+
*
|
|
183
|
+
* The caller is responsible for resolving the `callerId` (via
|
|
184
|
+
* `resolveCallerId` from `src/services/session/resolve-caller-id.ts`)
|
|
185
|
+
* and the `peakSessionId` (via `getCallerBinding` then reading
|
|
186
|
+
* `peakSessionId`, OR via `ensureSession` for the first-time case).
|
|
187
|
+
*/
|
|
188
|
+
export function setSkillPresenceForCaller(projectRootOverride, callerId, peakSessionId, skill, mode, gate) {
|
|
189
|
+
const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
|
|
190
|
+
const now = new Date().toISOString();
|
|
191
|
+
const presence = {
|
|
192
|
+
skill,
|
|
193
|
+
...(validatedMode ? { mode: validatedMode } : {}),
|
|
194
|
+
...(gate ? { gate } : {}),
|
|
195
|
+
...(peakSessionId ? { sessionId: peakSessionId } : {}),
|
|
196
|
+
...(callerId ? { outerSessionId: callerId } : {}),
|
|
197
|
+
setAt: now,
|
|
198
|
+
lastHeartbeat: now
|
|
199
|
+
};
|
|
200
|
+
const presencePath = getActiveSkillFileForCallerPath(resolveProjectRoot(projectRootOverride), peakSessionId, callerId);
|
|
201
|
+
const presenceDir = dirname(presencePath);
|
|
202
|
+
if (!existsSync(presenceDir)) {
|
|
203
|
+
mkdirSync(presenceDir, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
writeFileSync(presencePath, JSON.stringify(presence, null, 2), 'utf8');
|
|
206
|
+
// Skill-activation side effect: bring the memory store into existence for
|
|
207
|
+
// fresh projects. Same fail-open contract as the legacy path.
|
|
208
|
+
ensureMemoryBootstrap(resolveProjectRoot(projectRootOverride));
|
|
209
|
+
return presence;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Compute the per-caller active-skill file path. Re-exported for test
|
|
213
|
+
* ergonomics; canonical path lives in
|
|
214
|
+
* `src/services/session/caller-binding-service.ts` but inlined here to
|
|
215
|
+
* avoid a circular import (`caller-binding-service` reads
|
|
216
|
+
* `skill-presence-service` for the `setCallerBinding` integration in
|
|
217
|
+
* future slices; the inverse import would deadlock).
|
|
218
|
+
*/
|
|
219
|
+
function getActiveSkillFileForCallerPath(projectRoot, peakSessionId, callerId) {
|
|
220
|
+
return resolve(projectRoot, '.peaks', '_runtime', peakSessionId, `active-skill-${callerId}.json`);
|
|
221
|
+
}
|
|
163
222
|
export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
164
223
|
const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
|
|
165
224
|
const sessionId = getCurrentSessionId(projectRootOverride);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* format-md-compact — single source of truth for whitespace / decoration
|
|
3
|
+
* normalization across every CLI body-output path.
|
|
4
|
+
*
|
|
5
|
+
* Slice 023 (R3) — `peaks project memories:show`, `peaks retrospective show`,
|
|
6
|
+
* and `peaks request show` (per-artifact) all funnel their `body` field through
|
|
7
|
+
* this helper so the LLM-consumed output is free of blank-line padding,
|
|
8
|
+
* decorative `---` rules, and frontmatter `description:` field-name repeats
|
|
9
|
+
* while preserving every semantically meaningful construct (code fences,
|
|
10
|
+
* setext heading underlines, GFM table syntax, frontmatter, list indentation,
|
|
11
|
+
* inline emphasis, code spans).
|
|
12
|
+
*
|
|
13
|
+
* Pure function: no fs, no I/O. Easy to unit-test (see
|
|
14
|
+
* `tests/unit/shared/format-md-compact.test.ts`).
|
|
15
|
+
*/
|
|
16
|
+
export interface FormatMdCompactOptions {
|
|
17
|
+
/** Preserve code-fence (``` ... ```) and frontmatter (--- ... ---) content. Default true. */
|
|
18
|
+
preserveCodeBlocks?: boolean;
|
|
19
|
+
/** Preserve setext heading underlines (===, --- under a text line). Default true. */
|
|
20
|
+
preserveSetextHeadings?: boolean;
|
|
21
|
+
/** Preserve GFM table syntax (| ... | rows). Default true. */
|
|
22
|
+
preserveTables?: boolean;
|
|
23
|
+
/** Collapse 3+ blank lines to 1. Default true. */
|
|
24
|
+
collapseBlankLines?: boolean;
|
|
25
|
+
/** Strip trailing whitespace per line. Default true. */
|
|
26
|
+
stripTrailingWhitespace?: boolean;
|
|
27
|
+
/** Strip decorative `---` lines (lines surrounded by blanks OR trailing final line). Default true. */
|
|
28
|
+
stripDecorativeHorizontalRules?: boolean;
|
|
29
|
+
/** Strip frontmatter `description:` field-name repeat when same value already in body header. Default true. */
|
|
30
|
+
stripFrontmatterDescriptionRepeat?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function formatMdCompact(input: string, options?: FormatMdCompactOptions): string;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* format-md-compact — single source of truth for whitespace / decoration
|
|
3
|
+
* normalization across every CLI body-output path.
|
|
4
|
+
*
|
|
5
|
+
* Slice 023 (R3) — `peaks project memories:show`, `peaks retrospective show`,
|
|
6
|
+
* and `peaks request show` (per-artifact) all funnel their `body` field through
|
|
7
|
+
* this helper so the LLM-consumed output is free of blank-line padding,
|
|
8
|
+
* decorative `---` rules, and frontmatter `description:` field-name repeats
|
|
9
|
+
* while preserving every semantically meaningful construct (code fences,
|
|
10
|
+
* setext heading underlines, GFM table syntax, frontmatter, list indentation,
|
|
11
|
+
* inline emphasis, code spans).
|
|
12
|
+
*
|
|
13
|
+
* Pure function: no fs, no I/O. Easy to unit-test (see
|
|
14
|
+
* `tests/unit/shared/format-md-compact.test.ts`).
|
|
15
|
+
*/
|
|
16
|
+
const FENCE_TRIPLE_BACKTICK = '```';
|
|
17
|
+
const FENCE_TRIPLE_TILDE = '~~~';
|
|
18
|
+
const FENCE_MARKER_RE = /^(```+|~~~+)/;
|
|
19
|
+
const TABLE_ROW_RE = /^\s*\|.*\|\s*$/;
|
|
20
|
+
const TABLE_ALIGN_ROW_RE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)+\|?\s*$/;
|
|
21
|
+
const ATX_HEADING_RE = /^#{1,6}\s/;
|
|
22
|
+
const HEADING_LINE_RE = /^\S/;
|
|
23
|
+
const SETEXT_UNDERLINE_RE = /^=+\s*$|^-{2,}\s*$/;
|
|
24
|
+
export function formatMdCompact(input, options = {}) {
|
|
25
|
+
if (input.length === 0)
|
|
26
|
+
return input;
|
|
27
|
+
const opts = {
|
|
28
|
+
preserveCodeBlocks: options.preserveCodeBlocks ?? true,
|
|
29
|
+
preserveSetextHeadings: options.preserveSetextHeadings ?? true,
|
|
30
|
+
preserveTables: options.preserveTables ?? true,
|
|
31
|
+
collapseBlankLines: options.collapseBlankLines ?? true,
|
|
32
|
+
stripTrailingWhitespace: options.stripTrailingWhitespace ?? true,
|
|
33
|
+
stripDecorativeHorizontalRules: options.stripDecorativeHorizontalRules ?? true,
|
|
34
|
+
stripFrontmatterDescriptionRepeat: options.stripFrontmatterDescriptionRepeat ?? true
|
|
35
|
+
};
|
|
36
|
+
// 1. Split frontmatter from body (if any). The YAML block lives between
|
|
37
|
+
// the first line `---` and the second line `---`. We carry it through
|
|
38
|
+
// unchanged.
|
|
39
|
+
const fm = splitFrontmatter(input);
|
|
40
|
+
const body = fm.body;
|
|
41
|
+
// 2. Walk the body line-by-line, applying the protected-zone rules.
|
|
42
|
+
const normalizedBody = normalizeBody(body, opts);
|
|
43
|
+
// 3. Optional frontmatter `description:` repeat strip. The body's
|
|
44
|
+
// first ATX heading is compared to the frontmatter `description:`
|
|
45
|
+
// value; if the description text repeats the heading, the leading
|
|
46
|
+
// paragraph is dropped. Implemented as a one-shot post-pass.
|
|
47
|
+
const finalBody = opts.stripFrontmatterDescriptionRepeat
|
|
48
|
+
? stripDescriptionRepeat(normalizedBody, fm.description)
|
|
49
|
+
: normalizedBody;
|
|
50
|
+
// 4. Re-assemble. The frontmatter stays verbatim; the body carries the
|
|
51
|
+
// normalized text. Match the original layout: if the input had
|
|
52
|
+
// frontmatter, output is `frontmatter + blank + body`; otherwise
|
|
53
|
+
// the body is the whole output.
|
|
54
|
+
if (fm.raw === '') {
|
|
55
|
+
return finalBody;
|
|
56
|
+
}
|
|
57
|
+
return fm.raw + '\n' + finalBody;
|
|
58
|
+
}
|
|
59
|
+
function splitFrontmatter(input) {
|
|
60
|
+
// Normalize line endings so Windows \r\n doesn't confuse the leading-marker check.
|
|
61
|
+
const normalized = input.replace(/\r\n/g, '\n');
|
|
62
|
+
const lines = normalized.split('\n');
|
|
63
|
+
if (lines[0] !== '---') {
|
|
64
|
+
return { raw: '', description: null, body: normalized };
|
|
65
|
+
}
|
|
66
|
+
let closeIndex = -1;
|
|
67
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
68
|
+
if (lines[index] === '---') {
|
|
69
|
+
closeIndex = index;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (closeIndex < 0) {
|
|
74
|
+
return { raw: '', description: null, body: normalized };
|
|
75
|
+
}
|
|
76
|
+
const raw = lines.slice(0, closeIndex + 1).join('\n');
|
|
77
|
+
// Body = everything after the closing `---` (preserving one optional blank line).
|
|
78
|
+
const bodyLines = lines.slice(closeIndex + 1);
|
|
79
|
+
while (bodyLines.length > 0 && bodyLines[0] === '') {
|
|
80
|
+
bodyLines.shift();
|
|
81
|
+
}
|
|
82
|
+
const body = bodyLines.join('\n');
|
|
83
|
+
// Extract the `description:` field. Walk the YAML block; pull the value
|
|
84
|
+
// as a single-line string. Multi-line (`|`) or folded (`>`) scalars are
|
|
85
|
+
// joined with a single space — the description is a short summary, the
|
|
86
|
+
// exact whitespace inside the block scalar is not preserved.
|
|
87
|
+
const frontmatterLines = lines.slice(1, closeIndex);
|
|
88
|
+
const description = extractFrontmatterDescription(frontmatterLines);
|
|
89
|
+
return { raw, description, body };
|
|
90
|
+
}
|
|
91
|
+
function extractFrontmatterDescription(frontmatterLines) {
|
|
92
|
+
for (let index = 0; index < frontmatterLines.length; index += 1) {
|
|
93
|
+
const line = frontmatterLines[index] ?? '';
|
|
94
|
+
const match = line.match(/^description:\s*(.*)$/);
|
|
95
|
+
if (match === null)
|
|
96
|
+
continue;
|
|
97
|
+
const inline = (match[1] ?? '').trim();
|
|
98
|
+
if (inline === '|' || inline === '>') {
|
|
99
|
+
const collected = [];
|
|
100
|
+
for (let inner = index + 1; inner < frontmatterLines.length; inner += 1) {
|
|
101
|
+
const innerLine = frontmatterLines[inner] ?? '';
|
|
102
|
+
if (/^[A-Za-z0-9_-]+:/.test(innerLine))
|
|
103
|
+
break;
|
|
104
|
+
collected.push(innerLine.replace(/^\s{2}/, ''));
|
|
105
|
+
}
|
|
106
|
+
return collected.join(' ').trim();
|
|
107
|
+
}
|
|
108
|
+
// Strip surrounding quotes if the value is a quoted scalar.
|
|
109
|
+
return inline.replace(/^['"]|['"]$/g, '');
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function normalizeBody(body, opts) {
|
|
114
|
+
// Walk line-by-line, tracking the protected-zone flags and emitting a
|
|
115
|
+
// transformed line stream. We keep three flag bits:
|
|
116
|
+
// insideFence: toggled on ``` / ~~~ lines
|
|
117
|
+
// insideFrontmatterYAML: handled by splitFrontmatter; body has none.
|
|
118
|
+
// insideTable: detected by leading-pipe; carried until first non-pipe
|
|
119
|
+
// line.
|
|
120
|
+
const lines = body.split('\n');
|
|
121
|
+
// 1. Pre-pass: compute the `setextUnderlined` markers so the decorative
|
|
122
|
+
// `---` strip can know when a line is a setext H2 underline (semantic)
|
|
123
|
+
// vs a decoration.
|
|
124
|
+
const setextUnderlined = computeSetextUnderlines(lines);
|
|
125
|
+
const protection = [];
|
|
126
|
+
let insideFence = false;
|
|
127
|
+
let insideTable = false;
|
|
128
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
129
|
+
const line = lines[index] ?? '';
|
|
130
|
+
if (opts.preserveCodeBlocks && isFenceOpenLine(line) && !isFenceCloseLine(line, insideFence)) {
|
|
131
|
+
insideFence = !insideFence;
|
|
132
|
+
protection.push('fence');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (insideFence) {
|
|
136
|
+
protection.push('fence');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (opts.preserveSetextHeadings && setextUnderlined.has(index)) {
|
|
140
|
+
protection.push('setext');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (opts.preserveTables && isTableLine(line)) {
|
|
144
|
+
// Entering / continuing a table — first row that looks like a table
|
|
145
|
+
// starts the table zone; the alignment row (|---|) is included.
|
|
146
|
+
insideTable = true;
|
|
147
|
+
protection.push('table');
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (insideTable && !isTableLine(line) && line.trim() !== '') {
|
|
151
|
+
insideTable = false;
|
|
152
|
+
}
|
|
153
|
+
if (insideTable) {
|
|
154
|
+
protection.push('table');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
protection.push('plain');
|
|
158
|
+
}
|
|
159
|
+
// 3. Per-line transforms: strip trailing whitespace outside protected
|
|
160
|
+
// zones; strip decorative `---` outside setext / table / fence zones.
|
|
161
|
+
const out = [];
|
|
162
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
163
|
+
const line = lines[index] ?? '';
|
|
164
|
+
const zone = protection[index] ?? 'plain';
|
|
165
|
+
let transformed = line;
|
|
166
|
+
if (zone !== 'fence' && opts.stripTrailingWhitespace) {
|
|
167
|
+
transformed = transformed.replace(/[ \t]+$/u, '');
|
|
168
|
+
}
|
|
169
|
+
if (zone !== 'fence' &&
|
|
170
|
+
zone !== 'setext' &&
|
|
171
|
+
zone !== 'table' &&
|
|
172
|
+
opts.stripDecorativeHorizontalRules) {
|
|
173
|
+
if (isDecorativeHorizontalRule(transformed)) {
|
|
174
|
+
// Mark for removal: push a sentinel. The blank-line collapse step
|
|
175
|
+
// will merge surrounding blanks.
|
|
176
|
+
transformed = '__PEAKS_HR_REMOVED__';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
out.push(transformed);
|
|
180
|
+
}
|
|
181
|
+
// 4. Drop the sentinel lines (decorative `---` after classification).
|
|
182
|
+
let collapsed = out.filter((line) => line !== '__PEAKS_HR_REMOVED__');
|
|
183
|
+
// 5. Collapse 3+ consecutive blank lines into 1. Two blank lines
|
|
184
|
+
// between two non-blank lines are also collapsed to 1 (matches PRD
|
|
185
|
+
// R2's "decorative `---` is redundant" rule — the original visual
|
|
186
|
+
// gap that surrounded the `---` collapses with it).
|
|
187
|
+
if (opts.collapseBlankLines) {
|
|
188
|
+
collapsed = collapseMultiBlanks(collapsed);
|
|
189
|
+
}
|
|
190
|
+
return collapsed.join('\n');
|
|
191
|
+
}
|
|
192
|
+
function isFenceOpenLine(line) {
|
|
193
|
+
return FENCE_MARKER_RE.test(line);
|
|
194
|
+
}
|
|
195
|
+
function isFenceCloseLine(line, insideFence) {
|
|
196
|
+
// The `isFenceOpenLine` already returned true for this line, so it
|
|
197
|
+
// starts with ``` or ~~~. A "close" line is one that opens a new fence
|
|
198
|
+
// of the *same* length. We approximate by treating any opener as a close
|
|
199
|
+
// when we are currently inside a fence.
|
|
200
|
+
return insideFence;
|
|
201
|
+
}
|
|
202
|
+
function isTableLine(line) {
|
|
203
|
+
return TABLE_ROW_RE.test(line) || TABLE_ALIGN_ROW_RE.test(line);
|
|
204
|
+
}
|
|
205
|
+
function computeSetextUnderlines(lines) {
|
|
206
|
+
// A `===` or `---` line is a setext heading underline ONLY when it sits
|
|
207
|
+
// directly under a non-blank, non-ATX text line (no blank line between
|
|
208
|
+
// them). The presence of a blank line between the text and the rule
|
|
209
|
+
// disqualifies it (a blank-separated `---` is decoration, not setext).
|
|
210
|
+
const result = new Set();
|
|
211
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
212
|
+
const line = lines[index] ?? '';
|
|
213
|
+
if (!SETEXT_UNDERLINE_RE.test(line))
|
|
214
|
+
continue;
|
|
215
|
+
const prev = lines[index - 1] ?? '';
|
|
216
|
+
if (prev === '')
|
|
217
|
+
continue;
|
|
218
|
+
if (ATX_HEADING_RE.test(prev))
|
|
219
|
+
continue;
|
|
220
|
+
if (!HEADING_LINE_RE.test(prev))
|
|
221
|
+
continue;
|
|
222
|
+
result.add(index);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
function isDecorativeHorizontalRule(line) {
|
|
227
|
+
// A line that is exactly `---` (or any number of `-` chars) is a
|
|
228
|
+
// candidate horizontal rule. The caller has already excluded setext
|
|
229
|
+
// and table contexts via the `protection` array.
|
|
230
|
+
return /^-+$/.test(line);
|
|
231
|
+
}
|
|
232
|
+
function collapseMultiBlanks(lines) {
|
|
233
|
+
const result = [];
|
|
234
|
+
let blankRun = 0;
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
if (line === '') {
|
|
237
|
+
blankRun += 1;
|
|
238
|
+
// 1 blank line is the cap. Drop the rest.
|
|
239
|
+
if (blankRun <= 1) {
|
|
240
|
+
result.push(line);
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
blankRun = 0;
|
|
245
|
+
result.push(line);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
function stripDescriptionRepeat(body, description) {
|
|
250
|
+
if (description === null || description.length === 0)
|
|
251
|
+
return body;
|
|
252
|
+
// Find the first ATX heading line. If it equals the frontmatter
|
|
253
|
+
// description text, drop it. Also drop a description-matching paragraph
|
|
254
|
+
// that immediately follows the heading (PRD's "frontmatter description
|
|
255
|
+
// already in body header" case). All other content is preserved.
|
|
256
|
+
const lines = body.split('\n');
|
|
257
|
+
if (lines.length === 0)
|
|
258
|
+
return body;
|
|
259
|
+
let firstHeadingIndex = -1;
|
|
260
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
261
|
+
const line = lines[index] ?? '';
|
|
262
|
+
if (line.trim() === '')
|
|
263
|
+
continue;
|
|
264
|
+
if (ATX_HEADING_RE.test(line)) {
|
|
265
|
+
firstHeadingIndex = index;
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (firstHeadingIndex < 0)
|
|
270
|
+
return body;
|
|
271
|
+
const headingText = (lines[firstHeadingIndex] ?? '').replace(/^#{1,6}\s+/, '').trim();
|
|
272
|
+
if (headingText !== description)
|
|
273
|
+
return body;
|
|
274
|
+
// Walk past the heading, then past any blank lines, then past the
|
|
275
|
+
// matching description paragraph (if present). Anything after the
|
|
276
|
+
// description paragraph is the body we want to keep.
|
|
277
|
+
const tail = lines.slice(firstHeadingIndex + 1);
|
|
278
|
+
let cursor = 0;
|
|
279
|
+
// Skip blanks.
|
|
280
|
+
while (cursor < tail.length && (tail[cursor] ?? '') === '') {
|
|
281
|
+
cursor += 1;
|
|
282
|
+
}
|
|
283
|
+
// Read the first non-blank paragraph.
|
|
284
|
+
const paragraphStart = cursor;
|
|
285
|
+
while (cursor < tail.length && (tail[cursor] ?? '') !== '') {
|
|
286
|
+
cursor += 1;
|
|
287
|
+
}
|
|
288
|
+
const paragraph = tail.slice(paragraphStart, cursor);
|
|
289
|
+
if (paragraph.join(' ').trim() !== description) {
|
|
290
|
+
// The paragraph doesn't match the description — don't drop it.
|
|
291
|
+
cursor = paragraphStart;
|
|
292
|
+
}
|
|
293
|
+
const kept = tail.slice(cursor);
|
|
294
|
+
if (kept.length === 0)
|
|
295
|
+
return '';
|
|
296
|
+
return kept.join('\n');
|
|
297
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stale-policy — pure helper for stale detection and filtering on memory
|
|
3
|
+
* and (future) retrospective entries.
|
|
4
|
+
*
|
|
5
|
+
* Slice 023 (R3) applies this to memory only (`peaks project memories:show`
|
|
6
|
+
* and the underlying `readMemoryIndex` load path). The retrospective index
|
|
7
|
+
* loader shares the same shape (`updatedAt: string`) and will be wired to
|
|
8
|
+
* the same helper in a future slice.
|
|
9
|
+
*
|
|
10
|
+
* Behavior:
|
|
11
|
+
* - `isStale(updatedAt)` returns true when (now - updatedAt) >
|
|
12
|
+
* thresholdDays * 86_400_000. Strict greater-than: a 30-day-old
|
|
13
|
+
* entry is NOT stale.
|
|
14
|
+
* - `applyStalePolicy(entries)` adds `stale: boolean` and
|
|
15
|
+
* `ageDays: number` to every entry (computed at CLI load time, never
|
|
16
|
+
* persisted to source `.md` files) and filters out entries where
|
|
17
|
+
* `stale === true` unless `includeStale: true` is passed.
|
|
18
|
+
* - Missing `updatedAt` is treated as fresh (`stale: false`,
|
|
19
|
+
* `ageDays: 0`); we never throw, so older index.json entries that
|
|
20
|
+
* lack the field stay loadable.
|
|
21
|
+
*/
|
|
22
|
+
export interface StalePolicyOptions {
|
|
23
|
+
/** Reference clock (ms since epoch). Defaults to Date.now(). Injected for testability. */
|
|
24
|
+
now?: number;
|
|
25
|
+
/** Threshold in days. Default 30. */
|
|
26
|
+
thresholdDays?: number;
|
|
27
|
+
/** When true, keep stale entries in the returned array (with `stale: true` set). Default false. */
|
|
28
|
+
includeStale?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface StaleAnnotated<T> {
|
|
31
|
+
stale: boolean;
|
|
32
|
+
ageDays: number;
|
|
33
|
+
}
|
|
34
|
+
export type StaleAnnotatedEntry<T> = T & StaleAnnotated<T>;
|
|
35
|
+
export interface StalePolicyResult<T> {
|
|
36
|
+
/** Filtered array, with stale entries removed (unless includeStale=true). */
|
|
37
|
+
entries: StaleAnnotatedEntry<T>[];
|
|
38
|
+
/** Count of entries dropped as stale. */
|
|
39
|
+
droppedCount: number;
|
|
40
|
+
/** Total count before filtering. */
|
|
41
|
+
totalCount: number;
|
|
42
|
+
}
|
|
43
|
+
export declare const DAY_MS = 86400000;
|
|
44
|
+
export declare const DEFAULT_STALE_DAYS = 30;
|
|
45
|
+
/**
|
|
46
|
+
* Returns true when the entry is older than `thresholdDays`. Strict
|
|
47
|
+
* greater-than: an entry exactly at the threshold is NOT stale.
|
|
48
|
+
*
|
|
49
|
+
* A missing or unparseable `updatedAt` is treated as fresh (false). This
|
|
50
|
+
* is the "defensive — older index.json entries may lack the field" rule
|
|
51
|
+
* from PRD R4.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isStale(updatedAt: string | undefined | null, options?: StalePolicyOptions): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Age in days between `updatedAt` and `now` (default Date.now()).
|
|
56
|
+
* Returns 0 for a missing / unparseable `updatedAt`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function ageInDays(updatedAt: string | undefined | null, now?: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* Apply the stale policy to a list of entries. Each entry is augmented
|
|
61
|
+
* with `stale: boolean` and `ageDays: number` (immutably — a fresh
|
|
62
|
+
* object is returned per entry). Stale entries are dropped from
|
|
63
|
+
* `entries` unless `includeStale: true` is passed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyStalePolicy<T extends {
|
|
66
|
+
updatedAt?: string | null;
|
|
67
|
+
}>(entries: T[], options?: StalePolicyOptions): StalePolicyResult<T>;
|