@ccdd/core 3.1.2 → 3.2.0
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/README.md +3 -2
- package/dist/src/definitions.d.ts +13 -1
- package/dist/src/sdk.d.ts +7 -1
- package/dist/src/sdk.js +2 -0
- package/dist/src/sdk.js.map +1 -1
- package/dist/src/tools/contracts.d.ts +55 -6
- package/examples/generated-artifacts/README.md +55 -0
- package/examples/generated-artifacts/ccdd.config.ts +86 -0
- package/examples/generated-artifacts/scenarios/checkout.json +12 -0
- package/examples/generated-artifacts/scenarios/search.json +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Three terms explain the picture:
|
|
|
19
19
|
|
|
20
20
|
| Term | Meaning | Example |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| **Artifact** | A named file, folder, or group of materials to review. | `spec.md`, `tests/`, or
|
|
22
|
+
| **Artifact** | A named file, folder, captured data value, or group of materials to review. | `spec.md`, `tests/`, an image, or a generated scenario. |
|
|
23
23
|
| **Critic** | A check with one target, reference materials, and a reviewer. | “Does this design meet these requirements?” |
|
|
24
24
|
| **Artifact tool** | A way for a reviewer to inspect an Artifact. | Read text, view an image, or open a desktop application. |
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ Each Critic declares its target and references. These relationships form a direc
|
|
|
27
27
|
|
|
28
28
|
## What you do
|
|
29
29
|
|
|
30
|
-
1. **Name your materials.** Give each Artifact an ID, a type, and a file or
|
|
30
|
+
1. **Name your materials.** Give each Artifact an ID, a type, and a file/folder path or a source for generated data. Groups collect existing Artifacts.
|
|
31
31
|
2. **Connect tools.** Use the optional default tools or write your own `metadata` and `execute` function. Register them by Artifact type in `ccdd.config.ts`.
|
|
32
32
|
3. **Define checks.** For each Critic, choose the target, its references, the review criteria, and an Agent, Human, or Runtime reviewer.
|
|
33
33
|
4. **Request a review.** Read the findings, update your project, and request another check when needed.
|
|
@@ -72,6 +72,7 @@ Reviews use a fixed copy of the project by default. Records and generated output
|
|
|
72
72
|
- [Write an Artifact tool](examples/custom-text-reader/README.md) using the public tool contract.
|
|
73
73
|
- [Use default text, file, image, and desktop tools](packages/default-tools/README.md).
|
|
74
74
|
- [Review a group of materials](examples/artifact-groups/README.md), such as an effect description and its preview image.
|
|
75
|
+
- [Review generated scenario data](docs/generated-artifacts.md) through tools that read a fixed captured value.
|
|
75
76
|
- [Use Agent and Human reviewers](docs/reviewers.md), including credentials and result submission.
|
|
76
77
|
- [Explore the demo](docs/demo.md): Why → Spec → Tests → Implementation.
|
|
77
78
|
- [Look up commands, reuse rules, and exit codes](docs/project-validation.md).
|
|
@@ -46,7 +46,19 @@ export interface ArtifactGroupDefinition {
|
|
|
46
46
|
basis?: boolean;
|
|
47
47
|
stale?: StaleStrategy;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
/** Generated material is captured separately from configuration evaluation. */
|
|
50
|
+
export interface GeneratedArtifactDefinition {
|
|
51
|
+
kind: 'generated';
|
|
52
|
+
type: string;
|
|
53
|
+
source: string;
|
|
54
|
+
params?: import('./tools/contracts.js').JsonValue;
|
|
55
|
+
basis?: boolean;
|
|
56
|
+
stale?: {
|
|
57
|
+
kind: 'always';
|
|
58
|
+
};
|
|
59
|
+
path?: never;
|
|
60
|
+
}
|
|
61
|
+
export type ArtifactEntryDefinition = ArtifactDefinition | GeneratedArtifactDefinition | ArtifactGroupDefinition;
|
|
50
62
|
export interface ArtifactGroupReference {
|
|
51
63
|
id: string;
|
|
52
64
|
members: string[];
|
package/dist/src/sdk.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, ConfigFactory, InferSchema, JsonSchema, ToolDefinition, ToolMetadata } from './tools/contracts.js';
|
|
1
|
+
import type { ArtifactSourceDefinition, DataToolContext, Config, ConfigFactory, InferSchema, JsonSchema, ToolDefinition, ToolMetadata } from './tools/contracts.js';
|
|
2
2
|
export type * from './tools/contracts.js';
|
|
3
3
|
export type * from './definitions.js';
|
|
4
4
|
export declare function defineConfig<T extends Config | ConfigFactory>(config: T): T;
|
|
@@ -7,3 +7,9 @@ export declare function defineTool<const S extends JsonSchema>(tool: Omit<ToolDe
|
|
|
7
7
|
inputSchema: S;
|
|
8
8
|
};
|
|
9
9
|
}): ToolDefinition<InferSchema<S>>;
|
|
10
|
+
export declare function defineDataTool<const S extends JsonSchema>(tool: Omit<ToolDefinition<InferSchema<S>, DataToolContext>, 'metadata'> & {
|
|
11
|
+
metadata: Omit<ToolMetadata, 'inputSchema' | 'artifactKind'> & {
|
|
12
|
+
inputSchema: S;
|
|
13
|
+
};
|
|
14
|
+
}): ToolDefinition<InferSchema<S>, DataToolContext>;
|
|
15
|
+
export declare function defineArtifactSource(source: ArtifactSourceDefinition): ArtifactSourceDefinition;
|
package/dist/src/sdk.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export function defineConfig(config) { return config; }
|
|
2
2
|
export function defineTool(tool) { return tool; }
|
|
3
|
+
export function defineDataTool(tool) { return { ...tool, metadata: { ...tool.metadata, artifactKind: 'data' } }; }
|
|
4
|
+
export function defineArtifactSource(source) { return source; }
|
|
3
5
|
//# sourceMappingURL=sdk.js.map
|
package/dist/src/sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/sdk.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,YAAY,CAAmC,MAAS,IAAO,OAAO,MAAM,CAAC,CAAC,CAAC;AAC/F,MAAM,UAAU,UAAU,CAA6B,IAA6H,IAAoC,OAAO,IAAI,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/sdk.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,YAAY,CAAmC,MAAS,IAAO,OAAO,MAAM,CAAC,CAAC,CAAC;AAC/F,MAAM,UAAU,UAAU,CAA6B,IAA6H,IAAoC,OAAO,IAAI,CAAC,CAAC,CAAC;AACtO,MAAM,UAAU,cAAc,CAA6B,IAA+J,IAAqD,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;AAC1V,MAAM,UAAU,oBAAoB,CAAC,MAAgC,IAA8B,OAAO,MAAM,CAAC,CAAC,CAAC"}
|
|
@@ -9,7 +9,7 @@ export interface ToolMetadata {
|
|
|
9
9
|
inputSchema: JsonSchema;
|
|
10
10
|
resultKinds: ToolResultKind[];
|
|
11
11
|
observation: 'content' | 'none';
|
|
12
|
-
artifactKind?: 'file' | 'directory' | 'any';
|
|
12
|
+
artifactKind?: 'file' | 'directory' | 'any' | 'data';
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
/** Project-relative runtime files or directories whose bytes affect this tool. */
|
|
15
15
|
executionPaths?: string[];
|
|
@@ -50,10 +50,57 @@ export interface ToolResult {
|
|
|
50
50
|
detail?: string;
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/** Data tools receive only their bound, captured Artifact data. */
|
|
54
|
+
export interface DataToolContext {
|
|
55
|
+
artifactId: string;
|
|
56
|
+
outputDir: string;
|
|
57
|
+
tmpDir: string;
|
|
58
|
+
signal: AbortSignal;
|
|
59
|
+
readData(): JsonValue;
|
|
60
|
+
resolveExecutionPath(path: string): Promise<string>;
|
|
61
|
+
}
|
|
62
|
+
export interface ArtifactIdentityStrategy {
|
|
63
|
+
kind: 'canonical-data' | 'immutable-revision' | 'custom';
|
|
64
|
+
namespace: string;
|
|
65
|
+
version: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ArtifactSourceMetadata {
|
|
68
|
+
identity: ArtifactIdentityStrategy;
|
|
69
|
+
/** Queries may invoke only sources explicitly declaring read-only preparation. */
|
|
70
|
+
preparation: 'read-only' | 'explicit';
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface ArtifactSourceContext {
|
|
74
|
+
artifactId: string;
|
|
75
|
+
params: JsonValue;
|
|
76
|
+
signal: AbortSignal;
|
|
77
|
+
/** Resolve captured project files, never the mutable original copy source. */
|
|
78
|
+
resolvePath(path: string): Promise<string>;
|
|
79
|
+
}
|
|
80
|
+
export interface ArtifactSourceResult {
|
|
81
|
+
data: JsonValue;
|
|
82
|
+
revision?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ArtifactSourceDefinition {
|
|
85
|
+
metadata: ArtifactSourceMetadata;
|
|
86
|
+
prepare(context: ArtifactSourceContext): ArtifactSourceResult | Promise<ArtifactSourceResult>;
|
|
87
|
+
/** A custom identity asserts equivalence of the complete supplied data. */
|
|
88
|
+
fingerprint?(data: JsonValue): string | Promise<string>;
|
|
89
|
+
}
|
|
90
|
+
export interface PreparedArtifactData {
|
|
91
|
+
version: 1;
|
|
92
|
+
identity: ArtifactIdentityStrategy & {
|
|
93
|
+
fingerprint: string;
|
|
94
|
+
};
|
|
95
|
+
/** Integrity hash of the canonical data, independent of semantic equivalence. */
|
|
96
|
+
contentHash: string;
|
|
97
|
+
data: JsonValue;
|
|
98
|
+
revision?: string;
|
|
99
|
+
}
|
|
100
|
+
export interface ToolDefinition<Args = Record<string, unknown>, Context = ToolContext> {
|
|
54
101
|
metadata: ToolMetadata;
|
|
55
|
-
execute(context:
|
|
56
|
-
preflight?(context:
|
|
102
|
+
execute(context: Context, args: Args): ToolResult | Promise<ToolResult>;
|
|
103
|
+
preflight?(context: Context): {
|
|
57
104
|
ok: boolean;
|
|
58
105
|
message: string;
|
|
59
106
|
} | Promise<{
|
|
@@ -62,8 +109,8 @@ export interface ToolDefinition<Args = Record<string, unknown>> {
|
|
|
62
109
|
}>;
|
|
63
110
|
}
|
|
64
111
|
export interface ArtifactToolsConfig {
|
|
65
|
-
agentTools?: Record<string, ToolDefinition<any>>;
|
|
66
|
-
humanTools?: Record<string, ToolDefinition<any>>;
|
|
112
|
+
agentTools?: Record<string, ToolDefinition<any, any>>;
|
|
113
|
+
humanTools?: Record<string, ToolDefinition<any, any>>;
|
|
67
114
|
}
|
|
68
115
|
export interface EnvironmentRequirement {
|
|
69
116
|
description: string;
|
|
@@ -78,6 +125,7 @@ export interface Config {
|
|
|
78
125
|
artifactTypes: Record<string, ArtifactToolsConfig>;
|
|
79
126
|
critics: CriticDefinition[];
|
|
80
127
|
envRequirements?: Record<string, EnvironmentRequirement>;
|
|
128
|
+
artifactSources?: Record<string, ArtifactSourceDefinition>;
|
|
81
129
|
}
|
|
82
130
|
export type ConfigFactory = () => Config | Promise<Config>;
|
|
83
131
|
export interface ConfigManifest {
|
|
@@ -100,6 +148,7 @@ export interface ConfigManifest {
|
|
|
100
148
|
path: string;
|
|
101
149
|
hash: string;
|
|
102
150
|
}[];
|
|
151
|
+
sources?: Record<string, ArtifactSourceMetadata>;
|
|
103
152
|
}
|
|
104
153
|
type Properties<S> = S extends {
|
|
105
154
|
properties: infer P;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Review generated scenario data
|
|
2
|
+
|
|
3
|
+
This example captures two JSON scenarios as independent generated Artifacts. The scenario files are sample evidence for demonstrating review mechanics, not recorded product tests. Each Human Critic must inspect an overview and every listed detail, then make an actual judgment.
|
|
4
|
+
|
|
5
|
+
`scenarioSource.prepare` reads a file from the captured project through `context.resolvePath`. It returns structured data directly; CCDD persists the fixed value outside the reviewed workspace. The source does no generation, so it declares `preparation: 'read-only'` and supports `status` and `plan`.
|
|
6
|
+
|
|
7
|
+
The explicitly registered `overview` and `detail` tools use `context.readData()`. Each call receives a copy of the recorded value, including after worker restart. The full scenario determines identity even when a tool only returns one field. Initial prompts and monitor scope metadata do not contain that data.
|
|
8
|
+
|
|
9
|
+
## Run the example
|
|
10
|
+
|
|
11
|
+
Use Node 22 LTS, at least 22.19.0. These APIs require CCDD 3.2.0 or later. From the source repository, build and pack core and project:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm ci
|
|
15
|
+
npm run build
|
|
16
|
+
CCDD_GENERATED_PACKAGES=$(mktemp -d /tmp/ccdd-generated-packages.XXXXXX)
|
|
17
|
+
npm pack --ignore-scripts --pack-destination "$CCDD_GENERATED_PACKAGES"
|
|
18
|
+
npm pack --ignore-scripts --workspace @ccdd/project --pack-destination "$CCDD_GENERATED_PACKAGES"
|
|
19
|
+
|
|
20
|
+
CCDD_GENERATED_EXAMPLE=$(mktemp -d /tmp/ccdd-generated-example.XXXXXX)
|
|
21
|
+
cp -R examples/generated-artifacts "$CCDD_GENERATED_EXAMPLE/project"
|
|
22
|
+
cd "$CCDD_GENERATED_EXAMPLE/project"
|
|
23
|
+
npm init -y
|
|
24
|
+
npm pkg set type=module
|
|
25
|
+
npm install --ignore-scripts "$CCDD_GENERATED_PACKAGES"/*.tgz
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Inspect the current input and execute the two data tools without starting a review:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx ccdd-project plan checkout
|
|
32
|
+
npx ccdd-project tools check --artifact checkout --for human --tool overview --execute
|
|
33
|
+
npx ccdd-project tools check --artifact checkout --for human --tool detail --execute --args '{"id":"summary"}'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
These tool diagnostics prepare a new snapshot and report actual tool results. They do not create a verdict or demonstrate that the scenario passes its Critic.
|
|
37
|
+
|
|
38
|
+
Request both Human reviews and open the monitor:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx ccdd-project verify --all --human-inbox
|
|
42
|
+
npx ccdd-project monitor
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open the printed local address, choose the project and a Human request, and claim it. Execute `overview`, then call `detail` for every returned detail ID. Submit your verdict with a summary and evidence citing those IDs. Repeat for the other scenario. See the [Human reviewer guide](../../docs/reviewers.md#ask-a-person-to-review) for the claim and submission workflow.
|
|
46
|
+
|
|
47
|
+
The tools are also registered for Agents. To use them, change the desired Critic to an explicitly supported Agent profile and supply credentials as described in the [reviewer guide](../../docs/reviewers.md). An Agent review makes actual Provider calls. Runtime Critics do not support generated Artifact scopes.
|
|
48
|
+
|
|
49
|
+
## Inspect reuse
|
|
50
|
+
|
|
51
|
+
After completing real reviews, run `npx ccdd-project status`. Edit a detail in `scenarios/checkout.json` and run `npx ccdd-project plan --all`. Checkout now has changed input; an applicable passing search review can remain reusable because its captured data and Critic definition are unchanged. Changing an unread detail also changes checkout identity. Restoring equivalent checkout data can reuse its applicable historical evidence, subject to the existing latest-verdict and dependency rules.
|
|
52
|
+
|
|
53
|
+
Changing whitespace or object-key order preserves canonical JSON identity. Array order remains significant. Keep changing scenario data in these source files rather than importing it into shared config: imported config module changes conservatively affect effective Critic definitions.
|
|
54
|
+
|
|
55
|
+
See [generated Artifact contracts](../../docs/generated-artifacts.md) for identity strategies, canonicalization, size limits, preparation policy, and reopening behavior.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { defineArtifactSource, defineConfig, defineDataTool } from '@ccdd/core';
|
|
3
|
+
import type { JsonValue } from '@ccdd/core';
|
|
4
|
+
|
|
5
|
+
function scenarioData(value: JsonValue) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
7
|
+
|| typeof value.name !== 'string' || typeof value.objective !== 'string'
|
|
8
|
+
|| !Array.isArray(value.criteria) || value.criteria.some(item => typeof item !== 'string')
|
|
9
|
+
|| !Array.isArray(value.details) || value.details.some(item => !item || typeof item !== 'object'
|
|
10
|
+
|| Array.isArray(item) || typeof item.id !== 'string' || typeof item.observation !== 'string')) {
|
|
11
|
+
throw new Error('Expected a scenario with name, objective, criteria, and identified details.');
|
|
12
|
+
}
|
|
13
|
+
return value as { name: string; objective: string; criteria: string[]; details: { id: string; observation: string }[] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const scenarioSource = defineArtifactSource({
|
|
17
|
+
metadata: {
|
|
18
|
+
preparation: 'read-only',
|
|
19
|
+
identity: { kind: 'canonical-data', namespace: 'example/scenario', version: '1' },
|
|
20
|
+
},
|
|
21
|
+
async prepare(context) {
|
|
22
|
+
if (typeof context.params !== 'string') throw new Error('Expected a scenario file path.');
|
|
23
|
+
const data: JsonValue = JSON.parse(await readFile(await context.resolvePath(context.params), 'utf8'));
|
|
24
|
+
scenarioData(data);
|
|
25
|
+
return { data };
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const overview = defineDataTool({
|
|
30
|
+
metadata: {
|
|
31
|
+
description: 'Read the objective, criteria, and available detail IDs of {artifactName}.',
|
|
32
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
33
|
+
resultKinds: ['json'],
|
|
34
|
+
observation: 'content',
|
|
35
|
+
},
|
|
36
|
+
execute(context) {
|
|
37
|
+
const data = scenarioData(context.readData());
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: 'json', data: {
|
|
40
|
+
name: data.name, objective: data.objective, criteria: data.criteria,
|
|
41
|
+
detailIds: data.details.map(detail => detail.id),
|
|
42
|
+
} }],
|
|
43
|
+
observation: { kind: 'content' },
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const detail = defineDataTool({
|
|
49
|
+
metadata: {
|
|
50
|
+
description: 'Read one identified detail from the captured {artifactName} scenario.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object', properties: { id: { type: 'string', minLength: 1 } },
|
|
53
|
+
required: ['id'], additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
resultKinds: ['json'],
|
|
56
|
+
observation: 'content',
|
|
57
|
+
},
|
|
58
|
+
execute(context, args) {
|
|
59
|
+
const found = scenarioData(context.readData()).details.find(item => item.id === args.id);
|
|
60
|
+
if (!found) throw new Error('Unknown scenario detail.');
|
|
61
|
+
return { content: [{ type: 'json', data: found }], observation: { kind: 'content' } };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
artifactSources: { scenario: scenarioSource },
|
|
67
|
+
artifactTypes: {
|
|
68
|
+
scenario: { agentTools: { overview, detail }, humanTools: { overview, detail } },
|
|
69
|
+
},
|
|
70
|
+
artifacts: {
|
|
71
|
+
checkout: { kind: 'generated', type: 'scenario', source: 'scenario', params: 'scenarios/checkout.json' },
|
|
72
|
+
search: { kind: 'generated', type: 'scenario', source: 'scenario', params: 'scenarios/search.json' },
|
|
73
|
+
},
|
|
74
|
+
critics: [
|
|
75
|
+
{
|
|
76
|
+
id: 'checkout-review', title: 'Review the checkout scenario', target: 'checkout', deps: [],
|
|
77
|
+
profile: { kind: 'human' },
|
|
78
|
+
payload: { instruction: 'Read the overview of {checkout}, then inspect every listed detail. Decide whether the observations satisfy each criterion. Cite detail IDs and explain missing or contradictory evidence.' },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'search-review', title: 'Review the search scenario', target: 'search', deps: [],
|
|
82
|
+
profile: { kind: 'human' },
|
|
83
|
+
payload: { instruction: 'Read the overview of {search}, then inspect every listed detail. Decide whether the observations satisfy each criterion. Cite detail IDs and explain missing or contradictory evidence.' },
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Guest checkout",
|
|
3
|
+
"objective": "A guest can review the total before placing an order.",
|
|
4
|
+
"criteria": [
|
|
5
|
+
"The order summary shows both shipping and the final total.",
|
|
6
|
+
"The guest can return to the cart before placing the order."
|
|
7
|
+
],
|
|
8
|
+
"details": [
|
|
9
|
+
{ "id": "summary", "observation": "The summary shows items at $20, shipping at $5, and a final total of $25." },
|
|
10
|
+
{ "id": "navigation", "observation": "A Back to cart link appears beside the Place order button." }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Empty search results",
|
|
3
|
+
"objective": "A user can recover from a search that finds no items.",
|
|
4
|
+
"criteria": [
|
|
5
|
+
"The result view states that no items match the search.",
|
|
6
|
+
"The user can change the query without leaving the result view."
|
|
7
|
+
],
|
|
8
|
+
"details": [
|
|
9
|
+
{ "id": "message", "observation": "The result heading reads No matching items." },
|
|
10
|
+
{ "id": "query", "observation": "The original query remains in an editable search field above the heading." }
|
|
11
|
+
]
|
|
12
|
+
}
|
package/package.json
CHANGED