specrails-core 6.6.5 → 6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runtime/capabilities.d.ts +5 -2
- package/dist/agent-runtime/capabilities.js +8 -2
- package/dist/agent-runtime/change-scope.d.ts +2 -0
- package/dist/agent-runtime/change-scope.js +5 -0
- package/dist/agent-runtime/cli-executor.js +8 -1
- package/dist/agent-runtime/cli.d.ts +6 -1
- package/dist/agent-runtime/cli.js +15 -4
- package/dist/agent-runtime/compact/reviewer.js +3 -1
- package/dist/agent-runtime/config.js +16 -5
- package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/efficiency-summary.js +7 -1
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
- package/dist/agent-runtime/engine/cli.d.ts +12 -0
- package/dist/agent-runtime/engine/cli.js +15 -4
- package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-schema.js +1 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-types.js +1 -1
- package/dist/agent-runtime/engine/definition-validator.js +8 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
- package/dist/agent-runtime/engine/invocation-context.js +2 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
- package/dist/agent-runtime/engine/piece-registry.js +1 -1
- package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
- package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
- package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
- package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
- package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
- package/dist/agent-runtime/engine/pieces/index.js +2 -1
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
- package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
- package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
- package/dist/agent-runtime/engine/preflight.d.ts +2 -1
- package/dist/agent-runtime/engine/preflight.js +3 -1
- package/dist/agent-runtime/engine/regex.d.ts +12 -2
- package/dist/agent-runtime/engine/regex.js +25 -11
- package/dist/agent-runtime/engine/run-status.d.ts +34 -0
- package/dist/agent-runtime/engine/run-status.js +7 -1
- package/dist/agent-runtime/engine/runs.d.ts +56 -0
- package/dist/agent-runtime/engine/runs.js +88 -5
- package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
- package/dist/agent-runtime/engine/steering/inbox.js +21 -1
- package/dist/agent-runtime/engine/storage/private-path.js +7 -1
- package/dist/agent-runtime/evaluation.js +1 -1
- package/dist/agent-runtime/executor-types.d.ts +19 -0
- package/dist/agent-runtime/executor-types.js +10 -0
- package/dist/agent-runtime/failure-classification.d.ts +51 -0
- package/dist/agent-runtime/failure-classification.js +110 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
- package/dist/agent-runtime/graph/artifacts.js +5 -1
- package/dist/agent-runtime/graph/baseline.d.ts +50 -0
- package/dist/agent-runtime/graph/baseline.js +145 -0
- package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
- package/dist/agent-runtime/graph/capability-map.js +65 -0
- package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
- package/dist/agent-runtime/graph/clarifications.js +158 -0
- package/dist/agent-runtime/graph/nodes.js +178 -19
- package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
- package/dist/agent-runtime/graph/review-brief-record.js +26 -0
- package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
- package/dist/agent-runtime/graph/review-brief.js +127 -0
- package/dist/agent-runtime/graph/roles.js +5 -3
- package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
- package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
- package/dist/agent-runtime/graph/state.d.ts +8 -0
- package/dist/agent-runtime/index.d.ts +7 -0
- package/dist/agent-runtime/index.js +7 -0
- package/dist/agent-runtime/openspec.d.ts +1 -0
- package/dist/agent-runtime/openspec.js +1 -1
- package/dist/agent-runtime/project-rules.d.ts +139 -0
- package/dist/agent-runtime/project-rules.js +683 -0
- package/dist/agent-runtime/prompts.d.ts +32 -1
- package/dist/agent-runtime/prompts.js +182 -7
- package/dist/agent-runtime/role-routing.d.ts +2 -1
- package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
- package/dist/agent-runtime/session/application/active-session.js +163 -7
- package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
- package/dist/agent-runtime/session/domain/descriptor.js +5 -0
- package/dist/agent-runtime/session/domain/interruption.js +1 -0
- package/dist/agent-runtime/session/domain/policy.js +2 -0
- package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
- package/dist/agent-runtime/session/domain/redaction.js +71 -0
- package/dist/agent-runtime/session/domain/types.d.ts +17 -1
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
- package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
- package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
- package/dist/agent-runtime/session/host/protocol.js +10 -2
- package/dist/agent-runtime/session/host/server.js +6 -5
- package/dist/agent-runtime/session/index.d.ts +1 -1
- package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
- package/dist/agent-runtime/session/ports.d.ts +6 -2
- package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
- package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
- package/dist/installer/cli.js +10 -1
- package/dist/installer/commands/init.js +14 -3
- package/dist/installer/phases/scaffold.js +56 -5
- package/dist/pipeline/pipeline-state.d.ts +22 -0
- package/dist/pipeline/pipeline-state.js +55 -4
- package/dist/pipeline/spec-trace.d.ts +134 -0
- package/dist/pipeline/spec-trace.js +392 -0
- package/dist/shared/secret-redaction.d.ts +42 -0
- package/dist/shared/secret-redaction.js +184 -0
- package/integration-contract.json +177 -7
- package/package.json +7 -7
- package/schemas/agent-runtime.schema.json +3 -1
- package/schemas/workflow-definition.schema.json +1 -0
- package/templates/settings/codex-config.toml +3 -2
|
@@ -19,6 +19,8 @@ export function executorDescriptor(id, displayName) {
|
|
|
19
19
|
autonomousContinuation: false,
|
|
20
20
|
steer: false,
|
|
21
21
|
toolFiltering: false,
|
|
22
|
+
// No turn-level stop that keeps a session: abort the invocation, then run the input next.
|
|
23
|
+
interrupt: 'emulated',
|
|
22
24
|
usage: Object.freeze({ costUsd: 'per-turn', tokens: 'per-turn' }),
|
|
23
25
|
}),
|
|
24
26
|
});
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type ValidateFunction } from 'ajv/dist/2020.js';
|
|
2
2
|
/** Protocol versions this host speaks (docs/agent-sessions/protocol.md). */
|
|
3
3
|
export declare const SESSION_PROTOCOL_VERSIONS: readonly number[];
|
|
4
|
+
/** `session.send` deliveries this host accepts (mirrored into integration-contract.json). */
|
|
5
|
+
export declare const SESSION_DELIVERIES: readonly ["queue", "steer", "interrupt"];
|
|
6
|
+
/**
|
|
7
|
+
* Capabilities `initialize` advertises (mirrored into integration-contract.json).
|
|
8
|
+
* `delegation`: session.delegate / session.waitSubagents and subagentRuntime.mode 'delegated'.
|
|
9
|
+
* `flowSessions`: session.send delivery 'interrupt' and per-driver capabilities.interrupt.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SESSION_HOST_CAPABILITIES: Readonly<{
|
|
12
|
+
readonly sessions: 1;
|
|
13
|
+
readonly delegation: 1;
|
|
14
|
+
readonly flowSessions: 1;
|
|
15
|
+
}>;
|
|
4
16
|
/** Longest accepted request line. */
|
|
5
17
|
export declare const MAX_FRAME_BYTES: number;
|
|
6
18
|
type Schema = Record<string, unknown>;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
2
|
/** Protocol versions this host speaks (docs/agent-sessions/protocol.md). */
|
|
3
3
|
export const SESSION_PROTOCOL_VERSIONS = Object.freeze([1]);
|
|
4
|
+
/** `session.send` deliveries this host accepts (mirrored into integration-contract.json). */
|
|
5
|
+
export const SESSION_DELIVERIES = Object.freeze(['queue', 'steer', 'interrupt']);
|
|
6
|
+
/**
|
|
7
|
+
* Capabilities `initialize` advertises (mirrored into integration-contract.json).
|
|
8
|
+
* `delegation`: session.delegate / session.waitSubagents and subagentRuntime.mode 'delegated'.
|
|
9
|
+
* `flowSessions`: session.send delivery 'interrupt' and per-driver capabilities.interrupt.
|
|
10
|
+
*/
|
|
11
|
+
export const SESSION_HOST_CAPABILITIES = Object.freeze({ sessions: 1, delegation: 1, flowSessions: 1 });
|
|
4
12
|
/** Longest accepted request line. */
|
|
5
13
|
export const MAX_FRAME_BYTES = 2 * 1024 * 1024;
|
|
6
14
|
const id = { type: 'string', minLength: 1, maxLength: 200 };
|
|
@@ -20,7 +28,7 @@ const mcpServer = {
|
|
|
20
28
|
};
|
|
21
29
|
const limits = {
|
|
22
30
|
type: 'object', additionalProperties: false,
|
|
23
|
-
properties: Object.fromEntries(['idleMs', 'stallMs', 'backgroundMaxMs', 'turnInactivityMs', 'maxSettleHandoffs', 'settleDebounceMs'].map((key) => [key, { type: 'integer', minimum: 0 }])),
|
|
31
|
+
properties: Object.fromEntries(['idleMs', 'stallMs', 'backgroundMaxMs', 'turnInactivityMs', 'toolInactivityMs', 'maxSettleHandoffs', 'settleDebounceMs'].map((key) => [key, { type: 'integer', minimum: 0 }])),
|
|
24
32
|
};
|
|
25
33
|
const policy = {
|
|
26
34
|
type: 'object', additionalProperties: false, required: ['subagents'],
|
|
@@ -58,7 +66,7 @@ export const METHOD_SCHEMAS = Object.freeze({
|
|
|
58
66
|
},
|
|
59
67
|
oneOf: [{ required: ['resume'] }, { required: ['driver', 'model', 'cwd', 'policy'] }],
|
|
60
68
|
},
|
|
61
|
-
'session.send': { type: 'object', additionalProperties: false, required: ['sessionId', 'input'], properties: { sessionId: id, input: { type: 'object', additionalProperties: false, required: ['inputId', 'text', 'delivery'], properties: { inputId: id, text, attachments: { type: 'array', maxItems: 32, items: attachment }, delivery: { enum: [
|
|
69
|
+
'session.send': { type: 'object', additionalProperties: false, required: ['sessionId', 'input'], properties: { sessionId: id, input: { type: 'object', additionalProperties: false, required: ['inputId', 'text', 'delivery'], properties: { inputId: id, text, attachments: { type: 'array', maxItems: 32, items: attachment }, delivery: { enum: [...SESSION_DELIVERIES] } } } } },
|
|
62
70
|
'session.interrupt': { type: 'object', additionalProperties: false, required: ['sessionId'], properties: { sessionId: id } },
|
|
63
71
|
'session.stopSubagents': { type: 'object', additionalProperties: false, required: ['sessionId'], properties: { sessionId: id, subagentIds: { type: 'array', maxItems: 500, items: id } } },
|
|
64
72
|
'session.delegate': { type: 'object', additionalProperties: false, required: ['sessionId', 'description', 'prompt'], properties: { sessionId: id, description: { type: 'string', minLength: 1, maxLength: 200 }, prompt: { type: 'string', minLength: 1, maxLength: 64 * 1024 }, agentType: { type: 'string', minLength: 1, maxLength: 100 }, contextTurns: { type: 'integer', minimum: 0, maximum: 10 } } },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StringDecoder } from 'node:string_decoder';
|
|
2
2
|
import { SessionError, isSessionError } from '../domain/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { redactSessionSnapshot } from '../domain/redaction.js';
|
|
4
|
+
import { MAX_FRAME_BYTES, RPC_APPLICATION_ERROR, RPC_INVALID_PARAMS, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_PARSE_ERROR, SESSION_HOST_CAPABILITIES, SESSION_PROTOCOL_VERSIONS, validatorFor, } from './protocol.js';
|
|
4
5
|
/**
|
|
5
6
|
* JSON-RPC 2.0 server over NDJSON stdio (session protocol v1).
|
|
6
7
|
* One handler per method (closed table); each validates its params with a
|
|
@@ -27,7 +28,8 @@ export class SessionHost {
|
|
|
27
28
|
const service = runtime.service;
|
|
28
29
|
this.handlers = Object.freeze({
|
|
29
30
|
'initialize': async (params) => this.initialize(params),
|
|
30
|
-
|
|
31
|
+
// Live snapshots come from the in-memory session: redact them like the journal does.
|
|
32
|
+
'session.open': async (params) => { const opened = await service.open(params); return { ...opened, snapshot: redactSessionSnapshot(opened.snapshot) }; },
|
|
31
33
|
'session.send': async (params) => service.send(String(params.sessionId), params.input),
|
|
32
34
|
'session.interrupt': async (params) => service.interrupt(String(params.sessionId)),
|
|
33
35
|
'session.delegate': async (params) => service.delegate(String(params.sessionId), {
|
|
@@ -40,7 +42,7 @@ export class SessionHost {
|
|
|
40
42
|
'session.stopSubagents': async (params) => service.stopSubagents(String(params.sessionId), params.subagentIds),
|
|
41
43
|
'session.update': async (params) => { const { sessionId, ...changes } = params; return service.update(String(sessionId), changes); },
|
|
42
44
|
'session.close': async (params) => { await service.close(String(params.sessionId), String(params.reason)); return {}; },
|
|
43
|
-
'session.snapshot': async (params) => service.snapshot(String(params.sessionId)),
|
|
45
|
+
'session.snapshot': async (params) => redactSessionSnapshot(service.snapshot(String(params.sessionId))),
|
|
44
46
|
'session.events': async (params) => service.events(String(params.sessionId), Number(params.afterSeq), params.limit === undefined ? undefined : Number(params.limit)),
|
|
45
47
|
'session.list': async (params) => ({ sessions: service.list(params.state ?? 'open') }),
|
|
46
48
|
'host.ping': async () => ({ ...service.stats(), uptimeMs: Math.round(process.uptime() * 1000) }),
|
|
@@ -159,8 +161,7 @@ export class SessionHost {
|
|
|
159
161
|
protocolVersion: Math.max(...requested),
|
|
160
162
|
scope: this.runtime.scope,
|
|
161
163
|
runtime: this.runtime.identity,
|
|
162
|
-
|
|
163
|
-
capabilities: { sessions: 1, delegation: 1 },
|
|
164
|
+
capabilities: { ...SESSION_HOST_CAPABILITIES },
|
|
164
165
|
drivers: this.runtime.service.drivers(),
|
|
165
166
|
};
|
|
166
167
|
}
|
|
@@ -7,7 +7,7 @@ import type { Clock, DriverCatalog, Ids } from './ports.js';
|
|
|
7
7
|
export { SessionError, isSessionError, type SessionErrorCode } from './domain/errors.js';
|
|
8
8
|
export { SESSION_EVENT_TYPES, type SessionEvent, type SessionEventBody, type SessionEventEnvelope, type SessionEventType, type ToolActivity } from './domain/events.js';
|
|
9
9
|
export type { SessionSnapshot, SubagentNode, TurnRecord, InputRecord, OpenTurn } from './domain/snapshot.js';
|
|
10
|
-
export type { DriverCapabilities, DriverDescriptor, SessionLimits, SessionPolicy, Usage, McpServerSpec, Attachment } from './domain/types.js';
|
|
10
|
+
export type { DriverCapabilities, DriverDescriptor, InputDelivery, SessionLimits, SessionPolicy, Usage, McpServerSpec, Attachment } from './domain/types.js';
|
|
11
11
|
export type { SessionPolicyInput } from './domain/policy.js';
|
|
12
12
|
export { DEFAULT_LIMITS } from './domain/policy.js';
|
|
13
13
|
export { SessionService, type OpenParams, type SendParams } from './application/session-service.js';
|
|
@@ -4,6 +4,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
4
4
|
import { sessionsRoot } from '../../../shared/specrails-home.js';
|
|
5
5
|
import { PersistenceError, privateSqlitePath } from '../../engine/storage/private-path.js';
|
|
6
6
|
import { SessionError } from '../domain/errors.js';
|
|
7
|
+
import { redactSessionEvent, redactSessionSnapshot } from '../domain/redaction.js';
|
|
7
8
|
import { emptySnapshot, foldEvents } from '../domain/snapshot.js';
|
|
8
9
|
import { JOURNAL_MIGRATIONS, JOURNAL_SCHEMA_VERSION } from './schema.js';
|
|
9
10
|
export const JOURNAL_LEASE_MS = 60_000;
|
|
@@ -82,8 +83,11 @@ export class SqliteSessionJournal {
|
|
|
82
83
|
const current = this.loadSnapshot(sessionId);
|
|
83
84
|
if (!current)
|
|
84
85
|
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
// Fold the events as produced (input fingerprints must match the live
|
|
87
|
+
// session), then persist and return only redacted copies: what is
|
|
88
|
+
// stored, published and replayed is the same redacted stream.
|
|
89
|
+
const next = redactSessionSnapshot(foldEvents(current, events.map((event, index) => ({ sessionId, seq: current.lastSeq + index + 1, event }))));
|
|
90
|
+
const envelopes = events.map((event, index) => ({ sessionId, seq: current.lastSeq + index + 1, event: redactSessionEvent(event) }));
|
|
87
91
|
const insert = this.db.prepare('INSERT INTO events(session_id,seq,type,at,payload_json) VALUES (?,?,?,?,?)');
|
|
88
92
|
for (const envelope of envelopes)
|
|
89
93
|
insert.run(sessionId, envelope.seq, envelope.event.type, envelope.event.at, JSON.stringify(envelope.event));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SessionEvent, SessionEventEnvelope, ToolActivity } from './domain/events.js';
|
|
9
9
|
import type { SessionSnapshot } from './domain/snapshot.js';
|
|
10
|
-
import type { Attachment, DriverDescriptor, OutputChannel, SessionPolicy, SubagentKind, SubagentPhase } from './domain/types.js';
|
|
10
|
+
import type { Attachment, DriverDescriptor, InputDelivery, OutputChannel, SessionPolicy, SubagentKind, SubagentPhase } from './domain/types.js';
|
|
11
11
|
import type { ReportedUsage, UsageBaseline } from './domain/usage.js';
|
|
12
12
|
export interface DriverOpenSpec {
|
|
13
13
|
sessionId: string;
|
|
@@ -25,7 +25,11 @@ export interface DriverInput {
|
|
|
25
25
|
inputId: string;
|
|
26
26
|
text: string;
|
|
27
27
|
attachments?: Attachment[];
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Drivers receive `interrupt` only when they declare a native queue and a
|
|
30
|
+
* native interrupt: they stop the running turn and put this input first.
|
|
31
|
+
*/
|
|
32
|
+
delivery: InputDelivery;
|
|
29
33
|
/** Set by the application for policy-driven turns (e.g. collecting settled sub-agents). */
|
|
30
34
|
origin?: 'user' | 'system';
|
|
31
35
|
}
|
|
@@ -8,12 +8,12 @@ function assert(condition, message) {
|
|
|
8
8
|
if (!condition)
|
|
9
9
|
throw new Error(`Driver conformance: ${message}`);
|
|
10
10
|
}
|
|
11
|
-
async function oneTurn(harness) {
|
|
11
|
+
async function oneTurn(harness, delivery = 'queue') {
|
|
12
12
|
const factory = harness.factory();
|
|
13
13
|
const journal = new MemoryJournal();
|
|
14
14
|
const service = new SessionService({ journal, drivers: catalogOf(factory), clock: new FakeClock(), ids: new SequentialIds() });
|
|
15
15
|
const { sessionId } = await service.open({ driver: factory.descriptor.id, model: harness.model, cwd: process.cwd(), policy: { subagents: harness.subagents } });
|
|
16
|
-
await service.send(sessionId, { inputId: 'conformance-1', text: 'Reply briefly.', delivery
|
|
16
|
+
await service.send(sessionId, { inputId: 'conformance-1', text: 'Reply briefly.', delivery });
|
|
17
17
|
await harness.settle(factory);
|
|
18
18
|
const events = () => journal.events(sessionId).map((envelope) => envelope.event);
|
|
19
19
|
return { factory, journal, service, sessionId, events };
|
|
@@ -67,6 +67,20 @@ export const DRIVER_CONFORMANCE = Object.freeze([
|
|
|
67
67
|
assert(snapshot.openTurn === null && snapshot.phase === 'idle', 'the session is idle after a turn without sub-agents');
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'declares its interrupt mechanism and treats an interrupt on an idle session as a queued input',
|
|
72
|
+
async run(harness) {
|
|
73
|
+
const caps = harness.factory().descriptor.capabilities;
|
|
74
|
+
assert(caps.interrupt === 'native' || caps.interrupt === 'emulated', 'capabilities.interrupt is native or emulated');
|
|
75
|
+
const { events, service, sessionId } = await oneTurn(harness, 'interrupt');
|
|
76
|
+
const list = events();
|
|
77
|
+
const accepted = list.find((event) => event.type === 'input.accepted');
|
|
78
|
+
assert(accepted?.delivery === 'interrupt', 'the journal keeps the requested delivery');
|
|
79
|
+
const statuses = list.filter((event) => event.type === 'turn.completed').map((event) => event.status);
|
|
80
|
+
assert(statuses.length === 1 && statuses[0] === 'completed', 'nothing is interrupted on an idle session');
|
|
81
|
+
assert(service.snapshot(sessionId).inputs['conformance-1']?.state === 'completed', 'the input completes with its turn');
|
|
82
|
+
},
|
|
83
|
+
},
|
|
70
84
|
{
|
|
71
85
|
name: 'reports usage according to its declared semantics',
|
|
72
86
|
async run(harness) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactSessionEvent, redactSessionSnapshot } from '../domain/redaction.js';
|
|
1
2
|
import { emptySnapshot, foldEvents } from '../domain/snapshot.js';
|
|
2
3
|
/**
|
|
3
4
|
* In-memory SessionJournal with the same contract as the SQLite adapter:
|
|
@@ -23,8 +24,9 @@ export class MemoryJournal {
|
|
|
23
24
|
this.failNextAppend = null;
|
|
24
25
|
throw error;
|
|
25
26
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
27
|
+
const snapshot = redactSessionSnapshot(foldEvents(entry.snapshot, events.map((event, index) => ({ sessionId, seq: entry.snapshot.lastSeq + index + 1, event }))));
|
|
28
|
+
// Same redaction boundary as the SQLite adapter (domain/redaction.ts).
|
|
29
|
+
const envelopes = events.map((event, index) => ({ sessionId, seq: entry.snapshot.lastSeq + index + 1, event: structuredClone(redactSessionEvent(event)) }));
|
|
28
30
|
entry.events.push(...envelopes);
|
|
29
31
|
entry.snapshot = snapshot;
|
|
30
32
|
if (options?.baseline)
|
package/dist/installer/cli.js
CHANGED
|
@@ -25,7 +25,8 @@ function usageText() {
|
|
|
25
25
|
' install-framework Materialize the versioned framework (offline)',
|
|
26
26
|
' swap-current Point framework/current at a version (offline)',
|
|
27
27
|
' assemble Link the framework into a project workspace (offline)',
|
|
28
|
-
' pipeline Inspect and verify durable implementation phases',
|
|
28
|
+
' pipeline Inspect and verify durable implementation phases (pipeline trace: requirement ↔ test traceability)',
|
|
29
|
+
' rules Inventory project gates and written rules (rules inventory → .specrails/rules.json)',
|
|
29
30
|
' runtime Run, inspect and resume programmatic agents',
|
|
30
31
|
' help Show this help message',
|
|
31
32
|
' version Print the installed version',
|
|
@@ -57,6 +58,8 @@ async function dispatch(subcommand, flags, positionals) {
|
|
|
57
58
|
return runPipelineCommand(flags, positionals);
|
|
58
59
|
case 'runtime':
|
|
59
60
|
return (await import('../agent-runtime/cli.js')).runRuntimeCommand(flags, positionals);
|
|
61
|
+
case 'rules':
|
|
62
|
+
return (await import('../agent-runtime/project-rules.js')).runRulesCommand(flags, positionals);
|
|
60
63
|
case 'install-framework':
|
|
61
64
|
await runInstallFramework(flags);
|
|
62
65
|
return 0;
|
|
@@ -79,6 +82,12 @@ async function dispatch(subcommand, flags, positionals) {
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
export async function main(argv = process.argv.slice(2)) {
|
|
85
|
+
// `pipeline trace` keeps its own argv (repeatable, comma-separated lists) and
|
|
86
|
+
// judges execution with the runtime's full test-reachability check.
|
|
87
|
+
if (argv[0] === 'pipeline' && argv[1] === 'trace') {
|
|
88
|
+
const [{ runSpecTraceCli }, { commandReachesTest }] = await Promise.all([import('../pipeline/spec-trace.js'), import('../agent-runtime/compact/test-reachability.js')]);
|
|
89
|
+
return runSpecTraceCli(argv.slice(2), { reaches: commandReachesTest });
|
|
90
|
+
}
|
|
82
91
|
const { subcommand, flags, positionals } = parseArgs(argv);
|
|
83
92
|
// `--version` is global only before a command: `swap-current --version <v>`
|
|
84
93
|
// names a framework version.
|
|
@@ -6,7 +6,7 @@ import { recordSuccessfulInstall } from '../util/registry.js';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { InstallerError } from '../util/errors.js';
|
|
8
8
|
import { runCommand } from '../util/exec.js';
|
|
9
|
-
import { info, ok, step } from '../util/logger.js';
|
|
9
|
+
import { info, ok, step, warn } from '../util/logger.js';
|
|
10
10
|
import { isDir, isSymlink, listDir, pathExists, readTextFile, removePath, writeFileLf, } from '../util/fs.js';
|
|
11
11
|
import { loadInstallConfig, resolveConfigPath, } from '../phases/install-config.js';
|
|
12
12
|
import { checkPrerequisites } from '../phases/prereqs.js';
|
|
@@ -188,8 +188,8 @@ export async function runInit(flags) {
|
|
|
188
188
|
assertNoCoreDowngrade(readTextFile(installedMarker).trim(), version);
|
|
189
189
|
const previousSelections = snapshotWorkspaceProviderSelections(artifactRoot);
|
|
190
190
|
const surfaces = [
|
|
191
|
-
...['.claude', '.codex', '.gemini', '.kimi-code', 'AGENTS.md', 'GEMINI.md', '.gitignore'].map((name) => path.join(artifactRoot, name)),
|
|
192
|
-
...['specrails-version', 'specrails-manifest.json', 'setup-templates', 'runtime'].map((name) => path.join(artifactRoot, '.specrails', name)),
|
|
191
|
+
...['.claude', '.codex', '.gemini', '.kimi-code', 'AGENTS.md', 'GEMINI.md', 'CLAUDE.md', '.gitignore'].map((name) => path.join(artifactRoot, name)),
|
|
192
|
+
...['specrails-version', 'specrails-manifest.json', 'setup-templates', 'runtime', 'rules.json'].map((name) => path.join(artifactRoot, '.specrails', name)),
|
|
193
193
|
path.join(fwDir, 'current'),
|
|
194
194
|
// Contents deliberately NOT snapshotted: `installFramework` rebuilds a
|
|
195
195
|
// version tree whenever its content stamp does not match and stages
|
|
@@ -227,6 +227,17 @@ export async function runInit(flags) {
|
|
|
227
227
|
}
|
|
228
228
|
// openspec STAYS in the repo (codeRoot) — unchanged behaviour.
|
|
229
229
|
await installOpenSpecProject(codeRoot, prereqs.provider, artifactRoot);
|
|
230
|
+
// Project rules inventory (gates + parsed rule statements). Offline here:
|
|
231
|
+
// init never spends on a provider; classifications from an earlier
|
|
232
|
+
// `specrails-core rules inventory` are kept for unchanged rules.
|
|
233
|
+
try {
|
|
234
|
+
const { runRulesInventory } = await import('../../agent-runtime/project-rules.js');
|
|
235
|
+
const { inventory } = await runRulesInventory({ root: codeRoot, artifactRoot, classify: false });
|
|
236
|
+
ok(`Project rules inventory: ${inventory.gates.length} gate(s), ${inventory.rules.length} rule(s) in .specrails/rules.json`);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
warn(`Project rules inventory skipped: ${err.message}`);
|
|
240
|
+
}
|
|
230
241
|
if (artifactRoot !== codeRoot)
|
|
231
242
|
recordSuccessfulInstall(repoRoot, { home: process.env.SPECRAILS_REGISTRY_HOME, providers: [prereqs.provider], coreVersion: version });
|
|
232
243
|
step('Installation complete');
|
|
@@ -202,8 +202,15 @@ export function scaffoldInstallation(input) {
|
|
|
202
202
|
if (skills.placed > 0)
|
|
203
203
|
info(`Placed ${skills.placed} skill(s) into ${input.providerDir}/skills/`);
|
|
204
204
|
}
|
|
205
|
-
// ---
|
|
206
|
-
if (input.provider === '
|
|
205
|
+
// --- Provider settings + managed instruction block ---
|
|
206
|
+
if (input.provider === 'claude') {
|
|
207
|
+
const written = applyClaudeSettings(input);
|
|
208
|
+
copiedFiles += written;
|
|
209
|
+
if (written > 0) {
|
|
210
|
+
info(`Claude provider: wrote ${written} setting file(s) (CLAUDE.md)`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (input.provider === 'codex') {
|
|
207
214
|
const written = applyCodexSettings(input);
|
|
208
215
|
copiedFiles += written;
|
|
209
216
|
if (written > 0) {
|
|
@@ -544,7 +551,10 @@ function seedProjectLayer(input) {
|
|
|
544
551
|
// Project-named instruction file (codex AGENTS.md / gemini GEMINI.md). Reuse
|
|
545
552
|
// the same sentinel-upsert helpers via the settings appliers, scoped so they
|
|
546
553
|
// ONLY emit the instruction file (the settings file is already linked above).
|
|
547
|
-
if (input.provider === '
|
|
554
|
+
if (input.provider === 'claude') {
|
|
555
|
+
seedInstructionFile(path.join(input.workspace, 'CLAUDE.md'), renderInitialClaudeMd(input.codeRoot));
|
|
556
|
+
}
|
|
557
|
+
else if (input.provider === 'codex') {
|
|
548
558
|
seedInstructionFile(path.join(input.workspace, 'AGENTS.md'), renderInitialAgentsMd(input.codeRoot));
|
|
549
559
|
}
|
|
550
560
|
else if (input.provider === 'gemini') {
|
|
@@ -991,6 +1001,39 @@ function renderInitialKimiAgentsMd(repoRoot) {
|
|
|
991
1001
|
'',
|
|
992
1002
|
].join('\n');
|
|
993
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Claude Code provider settings: the sentinel-protected Specrails block in the
|
|
1006
|
+
* root `CLAUDE.md` (the instruction file Claude Code loads on startup), upserted
|
|
1007
|
+
* exactly like codex `AGENTS.md` and gemini `GEMINI.md` so user content outside
|
|
1008
|
+
* the block survives. Returns files written.
|
|
1009
|
+
*/
|
|
1010
|
+
function applyClaudeSettings(input) {
|
|
1011
|
+
const claudeMdPath = path.join(input.artifactRoot, 'CLAUDE.md');
|
|
1012
|
+
const content = renderInitialClaudeMd(input.codeRoot);
|
|
1013
|
+
if (!pathExists(claudeMdPath)) {
|
|
1014
|
+
writeFileLf(claudeMdPath, content);
|
|
1015
|
+
return 1;
|
|
1016
|
+
}
|
|
1017
|
+
const existing = readTextFile(claudeMdPath);
|
|
1018
|
+
const next = upsertAgentsMdManagedBlock(existing, extractManagedBlock(content));
|
|
1019
|
+
if (next === existing)
|
|
1020
|
+
return 0;
|
|
1021
|
+
writeFileLf(claudeMdPath, next);
|
|
1022
|
+
return 1;
|
|
1023
|
+
}
|
|
1024
|
+
function renderInitialClaudeMd(repoRoot) {
|
|
1025
|
+
return renderInitialAgentsMd(repoRoot).replace(AGENTS_MD_END, [
|
|
1026
|
+
'## Project rules',
|
|
1027
|
+
'',
|
|
1028
|
+
'- `.specrails/rules.json` (refreshed by `specrails-core rules inventory`)',
|
|
1029
|
+
' lists the gates this repository already enforces and how each written',
|
|
1030
|
+
' rule is checked. Run the gates that cover your change; rules marked',
|
|
1031
|
+
' `humanOnly` are for the human reviewer, never claim them verified.',
|
|
1032
|
+
'- Read provider rules from `.claude/rules/` when present.',
|
|
1033
|
+
'',
|
|
1034
|
+
AGENTS_MD_END,
|
|
1035
|
+
].join('\n'));
|
|
1036
|
+
}
|
|
994
1037
|
function renderInitialGeminiMd(repoRoot) {
|
|
995
1038
|
return renderInitialAgentsMd(repoRoot).replace(AGENTS_MD_END, 'Read provider rules from `.gemini/`.\n\n' + AGENTS_MD_END);
|
|
996
1039
|
}
|
|
@@ -1010,6 +1053,14 @@ function placePipelineRuntime(input) {
|
|
|
1010
1053
|
return 0;
|
|
1011
1054
|
const dest = path.join(input.artifactRoot, '.specrails', 'runtime');
|
|
1012
1055
|
copyFile(source, path.join(dest, 'pipeline-state.mjs'));
|
|
1056
|
+
// Requirement ↔ test traceability, self-contained like pipeline-state: `node .specrails/runtime/trace.mjs --check`.
|
|
1057
|
+
const trace = path.join(input.scriptDir, 'dist', 'pipeline', 'spec-trace.js');
|
|
1058
|
+
const traced = pathExists(trace) ? 2 : 0;
|
|
1059
|
+
if (traced) {
|
|
1060
|
+
copyFile(trace, path.join(dest, 'spec-trace.mjs'));
|
|
1061
|
+
writeFileLf(path.join(dest, 'trace.mjs'), "import { runSpecTraceCli } from './spec-trace.mjs'\n" +
|
|
1062
|
+
"process.exitCode = await runSpecTraceCli(process.argv.slice(2))\n");
|
|
1063
|
+
}
|
|
1013
1064
|
writeFileLf(path.join(dest, 'pipeline.mjs'), "import { runPipelineCli } from './pipeline-state.mjs'\n" +
|
|
1014
1065
|
"process.exitCode = await runPipelineCli(process.argv.slice(2))\n");
|
|
1015
1066
|
const runtime = path.join(input.scriptDir, 'dist', 'agent-runtime', 'cli.js');
|
|
@@ -1026,9 +1077,9 @@ function placePipelineRuntime(input) {
|
|
|
1026
1077
|
agents: Object.fromEntries(['architect', 'developer', 'reviewer'].map(role => [role, { provider, maxTurns: 100 }])), verification: [],
|
|
1027
1078
|
}, null, 2) + '\n');
|
|
1028
1079
|
}
|
|
1029
|
-
return 3;
|
|
1080
|
+
return 3 + traced;
|
|
1030
1081
|
}
|
|
1031
|
-
return 2;
|
|
1082
|
+
return 2 + traced;
|
|
1032
1083
|
}
|
|
1033
1084
|
function pruneLegacyArtifacts(input) {
|
|
1034
1085
|
const legacyPaths = [
|
|
@@ -147,10 +147,19 @@ export interface AcceptanceCheck {
|
|
|
147
147
|
scope: string;
|
|
148
148
|
limitations: string;
|
|
149
149
|
}
|
|
150
|
+
/** Where in the change one finding applies (contract 5.9). `finding` indexes `findings`; `path` is repository-relative. */
|
|
151
|
+
export interface FindingLocation {
|
|
152
|
+
finding: number;
|
|
153
|
+
path: string;
|
|
154
|
+
line?: number;
|
|
155
|
+
endLine?: number;
|
|
156
|
+
}
|
|
150
157
|
export interface AcceptanceReport {
|
|
151
158
|
criteria: AcceptanceCriterion[];
|
|
152
159
|
checks: AcceptanceCheck[];
|
|
153
160
|
findings: string[];
|
|
161
|
+
/** Optional anchors for findings (contract 5.9); absent when the reviewer named none. */
|
|
162
|
+
findingLocations?: FindingLocation[];
|
|
154
163
|
}
|
|
155
164
|
export interface AcceptanceReceipt extends AcceptanceReport {
|
|
156
165
|
scopeHash: string;
|
|
@@ -272,10 +281,23 @@ declare function inspectReceipt(state: PipelineState, env?: NodeJS.ProcessEnv):
|
|
|
272
281
|
reasons: string[];
|
|
273
282
|
receipt?: VerificationReceipt;
|
|
274
283
|
};
|
|
284
|
+
/**
|
|
285
|
+
* The requirement a spec without acceptance criteria freezes: its description
|
|
286
|
+
* minus the intent block. A hypothesis is validated with users after the
|
|
287
|
+
* change ships and its success signals are measured then; freezing them would
|
|
288
|
+
* make the reviewer demand evidence no code can give.
|
|
289
|
+
*/
|
|
290
|
+
export declare function requirementFromDescription(description: string, title: string): string;
|
|
275
291
|
/** The frozen requirements an acceptance report must certify, in scope order. */
|
|
276
292
|
export declare function frozenAcceptanceCriteria(contextInput: unknown): Array<Pick<AcceptanceCriterion, 'specId' | 'criterionIndex' | 'requirement'>>;
|
|
277
293
|
/** Validates a report against the frozen scope without recording it; throws the same errors `recordAcceptance` would. */
|
|
278
294
|
export declare function validateAcceptanceReport(contextInput: unknown, input: unknown): AcceptanceReport;
|
|
295
|
+
/**
|
|
296
|
+
* Keeps only well-formed anchors for the findings that exist. A malformed
|
|
297
|
+
* anchor is dropped, never fatal: a location is advisory and must not fail an
|
|
298
|
+
* otherwise valid review.
|
|
299
|
+
*/
|
|
300
|
+
export declare function sanitizeFindingLocations(value: unknown, findingCount: number): FindingLocation[];
|
|
279
301
|
declare function inspectAcceptance(state: PipelineState): {
|
|
280
302
|
valid: boolean;
|
|
281
303
|
status: PipelineCompletion['validation'];
|
|
@@ -296,7 +296,7 @@ export function bindImplementationExclusions(contextInput, exclusions) {
|
|
|
296
296
|
continue; // A scoped child receives the parent union, filtered to its frozen repositories.
|
|
297
297
|
repositoryExclusions[id] = [...new Set(paths)].sort();
|
|
298
298
|
for (const item of repositoryExclusions[id]) {
|
|
299
|
-
if (!/^(?:[a-zA-Z0-9][a-zA-Z0-9._-]*\/)*openspec\/(?:changes|specs)\/[a-z0-9][a-z0-9./-]
|
|
299
|
+
if (!/^(?:[a-zA-Z0-9][a-zA-Z0-9._-]*\/)*openspec\/(?:(?:changes|specs)\/[a-z0-9][a-z0-9./-]*|capability-map\.json)$/.test(item))
|
|
300
300
|
fail('Implementation artifact exclusion is outside declared OpenSpec artifacts');
|
|
301
301
|
safeChild(repository.path, item);
|
|
302
302
|
}
|
|
@@ -485,7 +485,8 @@ export function initializePipeline(contextInput, change) {
|
|
|
485
485
|
schemaVersion: 1, runId: context.runId, change, context, scopeHash: digest(canonical(context)), revision: 0,
|
|
486
486
|
createdAt: now, updatedAt: now,
|
|
487
487
|
phases: Object.fromEntries(PHASES.map((phase) => [phase, { status: 'pending' }])),
|
|
488
|
-
|
|
488
|
+
// The capability map is Core metadata the archive step rewrites (graph/capability-map.ts), never part of the reviewed candidate.
|
|
489
|
+
artifactExclusions: ['openspec/changes/' + change, 'openspec/capability-map.json'],
|
|
489
490
|
};
|
|
490
491
|
atomicJson(path.join(pipelineStateDirectory(context), 'context.json'), context);
|
|
491
492
|
saveState(state);
|
|
@@ -603,8 +604,26 @@ function taskGate(state) {
|
|
|
603
604
|
}
|
|
604
605
|
/** Stable indices refer to the frozen scope, never to rewritten design text.
|
|
605
606
|
* When no explicit AC list exists, the frozen description is the requirement. */
|
|
607
|
+
/** Heading of the intent block a host prepends to a spec's description (Desktop
|
|
608
|
+
* `renderIntentForAgents`): why and outcome, never a requirement to certify. */
|
|
609
|
+
const INTENT_BLOCK_HEADING = /^## Intent \(/m;
|
|
610
|
+
/**
|
|
611
|
+
* The requirement a spec without acceptance criteria freezes: its description
|
|
612
|
+
* minus the intent block. A hypothesis is validated with users after the
|
|
613
|
+
* change ships and its success signals are measured then; freezing them would
|
|
614
|
+
* make the reviewer demand evidence no code can give.
|
|
615
|
+
*/
|
|
616
|
+
export function requirementFromDescription(description, title) {
|
|
617
|
+
const start = description.search(INTENT_BLOCK_HEADING);
|
|
618
|
+
if (start === -1)
|
|
619
|
+
return description || title;
|
|
620
|
+
const rest = description.slice(start + 1);
|
|
621
|
+
const next = rest.search(/^## /m);
|
|
622
|
+
const stripped = (description.slice(0, start) + (next === -1 ? '' : rest.slice(next))).trim();
|
|
623
|
+
return stripped || title;
|
|
624
|
+
}
|
|
606
625
|
function frozenCriteria(context) {
|
|
607
|
-
return context.specs.flatMap(spec => (spec.acceptanceCriteria?.length ? spec.acceptanceCriteria : [spec.description
|
|
626
|
+
return context.specs.flatMap(spec => (spec.acceptanceCriteria?.length ? spec.acceptanceCriteria : [requirementFromDescription(spec.description, spec.title)])
|
|
608
627
|
.map((requirement, criterionIndex) => ({ specId: String(spec.id), criterionIndex, requirement })));
|
|
609
628
|
}
|
|
610
629
|
/** The frozen requirements an acceptance report must certify, in scope order. */
|
|
@@ -615,6 +634,36 @@ export function frozenAcceptanceCriteria(contextInput) {
|
|
|
615
634
|
export function validateAcceptanceReport(contextInput, input) {
|
|
616
635
|
return acceptanceReport(validatePipelineContext(contextInput), input);
|
|
617
636
|
}
|
|
637
|
+
const MAX_FINDING_LOCATIONS = 50;
|
|
638
|
+
/**
|
|
639
|
+
* Keeps only well-formed anchors for the findings that exist. A malformed
|
|
640
|
+
* anchor is dropped, never fatal: a location is advisory and must not fail an
|
|
641
|
+
* otherwise valid review.
|
|
642
|
+
*/
|
|
643
|
+
export function sanitizeFindingLocations(value, findingCount) {
|
|
644
|
+
if (!Array.isArray(value))
|
|
645
|
+
return [];
|
|
646
|
+
const out = [];
|
|
647
|
+
const seen = new Set();
|
|
648
|
+
for (const raw of value) {
|
|
649
|
+
if (out.length >= MAX_FINDING_LOCATIONS)
|
|
650
|
+
break;
|
|
651
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
652
|
+
continue;
|
|
653
|
+
const row = raw;
|
|
654
|
+
const finding = row.finding;
|
|
655
|
+
if (typeof finding !== 'number' || !Number.isInteger(finding) || finding < 0 || finding >= findingCount || seen.has(finding))
|
|
656
|
+
continue;
|
|
657
|
+
const filePath = typeof row.path === 'string' ? row.path.trim().replace(/\\/g, '/').replace(/^\.\//, '') : '';
|
|
658
|
+
if (!filePath || filePath.length > 500 || filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath) || filePath.includes('\0') || filePath.split('/').includes('..'))
|
|
659
|
+
continue;
|
|
660
|
+
const line = typeof row.line === 'number' && Number.isInteger(row.line) && row.line >= 1 && row.line <= 10_000_000 ? row.line : undefined;
|
|
661
|
+
const endLine = line !== undefined && typeof row.endLine === 'number' && Number.isInteger(row.endLine) && row.endLine > line && row.endLine <= 10_000_000 ? row.endLine : undefined;
|
|
662
|
+
seen.add(finding);
|
|
663
|
+
out.push({ finding, path: filePath, ...(line !== undefined ? { line } : {}), ...(endLine !== undefined ? { endLine } : {}) });
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
618
667
|
function nonempty(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
619
668
|
function evidence(value) { return Array.isArray(value) && value.length > 0 && value.every(nonempty); }
|
|
620
669
|
function acceptanceReport(context, input) {
|
|
@@ -655,7 +704,9 @@ function acceptanceReport(context, input) {
|
|
|
655
704
|
fail('Check needs status, evidence, measurement scope and limitations');
|
|
656
705
|
return { name: check.name, status: check.status, required: check.required, evidence: check.evidence, scope: check.scope, limitations: check.limitations };
|
|
657
706
|
});
|
|
658
|
-
|
|
707
|
+
const findings = report.findings;
|
|
708
|
+
const findingLocations = sanitizeFindingLocations(report.findingLocations, findings.length);
|
|
709
|
+
return { criteria, checks, findings, ...(findingLocations.length ? { findingLocations } : {}) };
|
|
659
710
|
}
|
|
660
711
|
function inspectAcceptance(state) {
|
|
661
712
|
const receipt = state.acceptance;
|