bmad-method-test-architecture-enterprise 3.2.0 → 3.4.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/.claude-plugin/marketplace.json +1 -1
- package/cli/evaluate.js +20 -0
- package/cli/lib/agent-adapters.js +46 -0
- package/cli/lib/evaluate/configuration-experiment.js +3401 -0
- package/cli/lib/evaluate/experiments.js +1299 -0
- package/cli/lib/evaluate/retain-skill-artifacts.js +78 -0
- package/cli/lib/evaluate/test-design-corpus/clean/docs/epics/epic-9-last-sync-indicator.md +47 -0
- package/cli/lib/evaluate/test-design-corpus/ground-truth.json +222 -0
- package/cli/lib/evaluate/test-design-corpus/native-identity.json +4 -0
- package/cli/lib/evaluate/test-design-corpus/seeded/docs/epics/epic-7-offline-order-capture.md +75 -0
- package/cli/lib/evaluate/test-design-replay.js +453 -0
- package/cli/lib/run-agent.js +18 -2
- package/cli/lib/test-design-staged-config.js +60 -0
- package/cli/lib/test-design-staging.js +229 -0
- package/cli/test-design-runner.js +245 -11
- package/package.json +4 -3
- package/skills/bmad-tea-automate/red/atdd-checklist-template.md +1 -1
- package/skills/bmad-tea-automate/red/resources/atdd-checklist-2-4-guest-checkout.example.md +1 -1
- package/skills/bmad-tea-automate/red/steps-c/step-01-preflight-and-context.md +1 -1
- package/skills/bmad-tea-automate/red/steps-c/step-05-validate-and-complete.md +1 -1
- package/skills/bmad-tea-evaluate/SKILL.md +11 -0
- package/skills/bmad-tea-evaluate/references/configuration-experiments.md +37 -0
- package/skills/bmad-tea-test-design/checklist.md +39 -8
- package/skills/bmad-tea-test-design/steps-c/step-03-risk-and-testability.md +1 -1
- package/skills/bmad-tea-test-design/steps-c/step-04-coverage-plan.md +10 -3
- package/skills/bmad-tea-test-design/steps-c/step-05-generate-output.md +69 -21
- package/skills/bmad-tea-test-design/steps-e/step-02-apply-edit.md +3 -0
- package/skills/bmad-tea-test-design/steps-v/step-01-validate.md +1 -0
- package/skills/bmad-tea-test-design/test-design-qa-template.md +1 -1
- package/skills/bmod-tea/bmod.toml +1 -1
- package/skills/bmod-tea/help/help.md +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Preserve generated workspaces and raw observations before eval cleanup. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const writeText = (file, text) => fs.promises.writeFile(file, text);
|
|
6
|
+
/** Observe each attempt before retry cleanup can discard its workspace. */
|
|
7
|
+
function captureProbe(port, record) {
|
|
8
|
+
return {
|
|
9
|
+
async probe(request, signal) {
|
|
10
|
+
try {
|
|
11
|
+
const observation = await port.probe(request, signal);
|
|
12
|
+
record(observation);
|
|
13
|
+
return observation;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
record({ fault: { name: error.name, code: error.code, message: error.message, detail: error.detail } });
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Retain exact workspace bytes, prompt, raw observation and live provenance. */
|
|
22
|
+
async function retainSkillArtifacts({
|
|
23
|
+
artifactsDir,
|
|
24
|
+
workspace,
|
|
25
|
+
caseId,
|
|
26
|
+
agent,
|
|
27
|
+
model,
|
|
28
|
+
repetition,
|
|
29
|
+
attempt,
|
|
30
|
+
prompt,
|
|
31
|
+
observation,
|
|
32
|
+
provenance = {},
|
|
33
|
+
preserveOnFailure = false,
|
|
34
|
+
}) {
|
|
35
|
+
if (!artifactsDir) return null;
|
|
36
|
+
const root = path.resolve(artifactsDir);
|
|
37
|
+
fs.mkdirSync(root, { recursive: true });
|
|
38
|
+
const label = `${agent}-${caseId}-run-${repetition}-attempt-${attempt}`.replaceAll(/[^a-zA-Z0-9_-]/g, '_');
|
|
39
|
+
const destination = fs.mkdtempSync(path.join(root, `${label}-`));
|
|
40
|
+
try {
|
|
41
|
+
fs.cpSync(workspace.dir, path.join(destination, 'workspace'), { recursive: true });
|
|
42
|
+
await writeText(path.join(destination, 'prompt.txt'), prompt);
|
|
43
|
+
if (observation !== undefined) await writeText(path.join(destination, 'observation.json'), `${JSON.stringify(observation, null, 2)}\n`);
|
|
44
|
+
await writeText(
|
|
45
|
+
path.join(destination, 'provenance.json'),
|
|
46
|
+
`${JSON.stringify({ mode: 'live', caseId, agent, requestedModel: model, repetition, attempt, capturedAt: new Date().toISOString(), originalWorkspace: workspace.dir, ...provenance }, null, 2)}\n`,
|
|
47
|
+
);
|
|
48
|
+
process.stderr.write(`retained skill artifacts: ${destination}\n`);
|
|
49
|
+
return destination;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (preserveOnFailure) {
|
|
52
|
+
error.retainedWorkspace = workspace.dir;
|
|
53
|
+
error.partialArchive = destination;
|
|
54
|
+
} else fs.rmSync(destination, { recursive: true, force: true });
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Clean the attempt; explicit experiment capture preserves it when retention fails. */
|
|
59
|
+
async function finishSkillAttempt({ retain, workspace, runError, remove = fs.rmSync, preserveOnFailure = false }) {
|
|
60
|
+
let failure = runError;
|
|
61
|
+
let retained = false;
|
|
62
|
+
try {
|
|
63
|
+
await retain();
|
|
64
|
+
retained = true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (failure) process.stderr.write(`artifact retention failed: ${error.message}\n`);
|
|
67
|
+
else failure = error;
|
|
68
|
+
} finally {
|
|
69
|
+
try {
|
|
70
|
+
if (retained || !preserveOnFailure) remove(workspace.dir, { recursive: true, force: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (failure) process.stderr.write(`attempt cleanup failed: ${error.message}\n`);
|
|
73
|
+
else failure = error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (failure && failure !== runError) throw failure;
|
|
77
|
+
}
|
|
78
|
+
module.exports = { retainSkillArtifacts, captureProbe, finishSkillAttempt };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Epic 9: Show the last successful sync time on the technician home screen
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Technicians ask dispatch whether their device is up to date. The answer already exists on the
|
|
6
|
+
device: the sync engine records the completion time of every successful sync. Nothing surfaces
|
|
7
|
+
it, so the question is asked over the radio instead.
|
|
8
|
+
|
|
9
|
+
This epic renders that value on the home screen. It adds one row to a screen that is already
|
|
10
|
+
rendered, and it reads a field the sync engine already writes.
|
|
11
|
+
|
|
12
|
+
## Stories
|
|
13
|
+
|
|
14
|
+
### 9.1 Render the last successful sync time
|
|
15
|
+
|
|
16
|
+
The home screen reads the existing `lastSuccessfulSyncAt` field from the local settings store
|
|
17
|
+
and renders it as a relative time, for example "synced 4 minutes ago".
|
|
18
|
+
|
|
19
|
+
The read is a single local key lookup on a store the screen already opens for other fields. No
|
|
20
|
+
network call is added, nothing is written, and no new field is stored.
|
|
21
|
+
|
|
22
|
+
When the field is absent, which is the state of a device that has never completed a sync, the
|
|
23
|
+
row is not rendered at all and the rest of the screen is unchanged.
|
|
24
|
+
|
|
25
|
+
### 9.2 Keep the value current while the screen is open
|
|
26
|
+
|
|
27
|
+
The row re-renders when the sync engine publishes a completion event, which it already
|
|
28
|
+
publishes for the existing progress spinner. No polling and no timer is added.
|
|
29
|
+
|
|
30
|
+
## Constraints already satisfied
|
|
31
|
+
|
|
32
|
+
- The value is a timestamp the device wrote about itself. It is shown to the person holding
|
|
33
|
+
the device, so the change exposes nothing to anyone who could not already see it, and it
|
|
34
|
+
adds no personal data to the screen.
|
|
35
|
+
- The change writes nothing. The settings store is opened read-only for this field, so no
|
|
36
|
+
order, queue entry or setting can be altered or lost by it.
|
|
37
|
+
- The change adds no network call and no storage. The single local key lookup runs on a screen
|
|
38
|
+
that is already open, so the screen's existing render budget is unchanged.
|
|
39
|
+
- The row ships behind `home_last_sync_row`, an existing feature flag the release process
|
|
40
|
+
already toggles per organization, so it can be turned off without a new build.
|
|
41
|
+
|
|
42
|
+
## Acceptance criteria
|
|
43
|
+
|
|
44
|
+
- A device that has completed a sync shows the relative time of the most recent one.
|
|
45
|
+
- A device that has never completed a sync shows the home screen with no sync row.
|
|
46
|
+
- The displayed value updates when a sync completes while the screen is open.
|
|
47
|
+
- Turning `home_last_sync_row` off restores the previous home screen exactly.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"The expected answer for the test-design behavioral eval, and the file the agent must never see.",
|
|
4
|
+
"",
|
|
5
|
+
"Two fixture sets, each one epic document and each its own case. The seeded set carries five",
|
|
6
|
+
"material risks the epic actually supports and four plausible risks it rules out in as many words.",
|
|
7
|
+
"The clean set carries a feature whose genuine risk set is small, which is what stops recall alone",
|
|
8
|
+
"from rewarding a run that reports everything it can think of.",
|
|
9
|
+
"",
|
|
10
|
+
"GROUNDING IS A SUBSTRING TEST, NOT A JUDGMENT",
|
|
11
|
+
"",
|
|
12
|
+
"Every material risk names the sentence of the epic that supports it in `groundingQuote`, and every",
|
|
13
|
+
"unsupported risk names the sentence that rules it out in `exclusionQuote`. --validate-only checks",
|
|
14
|
+
"each quote is present in the epic it names, with runs of whitespace collapsed on both sides so a",
|
|
15
|
+
"quote that spans a wrapped line still resolves. A fixture claiming the description supports a risk",
|
|
16
|
+
"the description never mentions fails before a model call is spent.",
|
|
17
|
+
"",
|
|
18
|
+
"A reported risk row matches a declared risk when its category is one the risk admits and every",
|
|
19
|
+
"group in `anyOf` has at least one token present in the row's description, case-insensitively. The",
|
|
20
|
+
"groups are an AND of ORs on purpose: one distinctive word is a coincidence and two are a claim.",
|
|
21
|
+
"Material risks are matched first and a matched row is consumed, so a row that names a real risk in",
|
|
22
|
+
"words that also appear in an exclusion is never counted twice.",
|
|
23
|
+
"",
|
|
24
|
+
"SEVERITY RANKS CONSTRAIN PAIRS, NOT LEVELS",
|
|
25
|
+
"",
|
|
26
|
+
"`severityRank` is an ordering and not a priority. Priority is a separate judgment the workflow",
|
|
27
|
+
"makes from business impact, user reach and workaround availability, which the risk score informs",
|
|
28
|
+
"and does not determine; `{tea-knowledge}/test-priorities-matrix.md` states that directly and",
|
|
29
|
+
"`{tea-knowledge}/probability-impact.md` repeats it. Asserting an absolute priority per risk",
|
|
30
|
+
"would measure this fixture's opinion rather than the workflow's rule, so what is checked instead is",
|
|
31
|
+
"that every pair of risks whose ranks are strictly ordered carries priorities in the same order.",
|
|
32
|
+
"Equal ranks constrain nothing and produce no pair.",
|
|
33
|
+
"",
|
|
34
|
+
"PROVENANCE",
|
|
35
|
+
"",
|
|
36
|
+
"The six categories, the 1-3 probability and impact scales and the P x I score are the workflow's",
|
|
37
|
+
"own, read from steps-c/step-03-risk-and-testability.md section 2 and from the two knowledge",
|
|
38
|
+
"fragments it names. The five test levels are the four steps-c/step-04-coverage-plan.md section 1",
|
|
39
|
+
"declares plus Integration, which {tea-knowledge}/test-levels-framework.md treats as a first",
|
|
40
|
+
"class level throughout. Both epics were written for this corpus and describe no real system.",
|
|
41
|
+
"",
|
|
42
|
+
"EVERY SET DECLARES A CEILING",
|
|
43
|
+
"",
|
|
44
|
+
"`maxRisks` bounds how many risks a run may report, and it is on both sets. It was on the clean",
|
|
45
|
+
"control alone, which left over-reporting on the seeded set unbounded: four risks this fixture never",
|
|
46
|
+
"anticipated could be appended to an otherwise correct run and every threshold still passed, because",
|
|
47
|
+
"precision counts only the rows matching a risk the epic rules out in as many words. The seeded",
|
|
48
|
+
"ceiling is 8 against 5 material risks, which leaves room for a real risk this fixture did not",
|
|
49
|
+
"foresee and stops a register that lists everything."
|
|
50
|
+
],
|
|
51
|
+
"riskCategories": ["TECH", "SEC", "PERF", "DATA", "BUS", "OPS"],
|
|
52
|
+
"testLevels": ["E2E", "API", "Component", "Integration", "Unit"],
|
|
53
|
+
"priorities": ["P0", "P1", "P2", "P3"],
|
|
54
|
+
"fixtureSets": [
|
|
55
|
+
{
|
|
56
|
+
"id": "seeded-offline-order-capture",
|
|
57
|
+
"root": "seeded",
|
|
58
|
+
"projectRoot": "field-order-capture",
|
|
59
|
+
"epicNum": "7",
|
|
60
|
+
"epicFile": "docs/epics/epic-7-offline-order-capture.md",
|
|
61
|
+
"maxRisks": 8,
|
|
62
|
+
"materialRisks": [
|
|
63
|
+
{
|
|
64
|
+
"id": "sec-unencrypted-offline-queue",
|
|
65
|
+
"categories": ["SEC", "DATA"],
|
|
66
|
+
"severityRank": 1,
|
|
67
|
+
"summary": "The outbound queue holds a card reference and a billing address unencrypted on the device.",
|
|
68
|
+
"groundingQuote": "It holds the full order payload, including the customer's stored card reference and billing address, and it is not encrypted at rest.",
|
|
69
|
+
"anyOf": [
|
|
70
|
+
["unencrypted", "not encrypted", "plaintext", "plain text", "cleartext", "encryption", "encrypt"],
|
|
71
|
+
["queue", "local", "device", "sqlite", "on-disk", "at rest", "storage", "stored"]
|
|
72
|
+
],
|
|
73
|
+
"acceptableCoverage": ["Unit", "Integration", "E2E"],
|
|
74
|
+
"exampleDescription": "The outbound queue is stored unencrypted on the device, so anyone reaching the local file can read the stored card reference."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "data-sync-conflict-overwrite",
|
|
78
|
+
"categories": ["DATA", "TECH", "BUS"],
|
|
79
|
+
"severityRank": 2,
|
|
80
|
+
"summary": "Two offline edits to one work order silently overwrite each other, because the server orders by arrival and checks no version.",
|
|
81
|
+
"groundingQuote": "The server applies queued edits in the order they arrive and performs no version check and no timestamp check, so a later arrival overwrites an earlier one",
|
|
82
|
+
"anyOf": [
|
|
83
|
+
["conflict", "overwrite", "overwrit", "clobber", "erase", "discard", "last write", "last-write", "lost update"],
|
|
84
|
+
["concurrent", "simultaneous", "two technicians", "second device", "same work order", "version", "timestamp"]
|
|
85
|
+
],
|
|
86
|
+
"acceptableCoverage": ["API", "Integration", "E2E"],
|
|
87
|
+
"exampleDescription": "Concurrent offline edits to the same work order overwrite each other on sync, because the server applies queued edits by arrival with no version check."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "tech-uncapped-retry-loop",
|
|
91
|
+
"categories": ["TECH", "OPS", "PERF"],
|
|
92
|
+
"severityRank": 3,
|
|
93
|
+
"summary": "A payload the server will never accept is retried forever with no backoff, cap or dead-letter path.",
|
|
94
|
+
"groundingQuote": "There is no backoff, no attempt cap, and no dead-letter path",
|
|
95
|
+
"anyOf": [
|
|
96
|
+
["retry", "retries", "retried", "backoff", "back-off", "dead letter", "dead-letter"],
|
|
97
|
+
["cap", "unbounded", "indefinit", "infinite", "loop", "storm", "exhaust", "no limit", "forever"]
|
|
98
|
+
],
|
|
99
|
+
"acceptableCoverage": ["Unit", "Integration", "API"],
|
|
100
|
+
"exampleDescription": "A rejected payload is retried forever with no backoff and no attempt cap, so one bad item loops until the application is killed."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "perf-backlog-sync-budget",
|
|
104
|
+
"categories": ["PERF", "TECH"],
|
|
105
|
+
"severityRank": 4,
|
|
106
|
+
"summary": "A full day's backlog of 2,000 queued items has to finish syncing within 30 seconds on mid-range hardware.",
|
|
107
|
+
"groundingQuote": "requires a full backlog of that size to finish syncing within 30 seconds of regaining connectivity",
|
|
108
|
+
"anyOf": [
|
|
109
|
+
["backlog", "2,000", "2000", "bulk", "volume", "throughput", "large queue"],
|
|
110
|
+
["30 second", "30s", "latency", "slow", "budget", "within 30", "sync time", "duration"]
|
|
111
|
+
],
|
|
112
|
+
"acceptableCoverage": ["API", "Integration", "E2E"],
|
|
113
|
+
"exampleDescription": "A 2000 item backlog may not finish syncing inside the 30 second budget on the mid-range hardware the fleet carries."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "ops-no-release-kill-switch",
|
|
117
|
+
"categories": ["OPS", "BUS"],
|
|
118
|
+
"severityRank": 4,
|
|
119
|
+
"summary": "The feature ships on for everyone with no flag, so recovery costs a store review of one to three days.",
|
|
120
|
+
"groundingQuote": "There is no feature flag, no staged rollout, and no way to disable it without shipping a new build",
|
|
121
|
+
"anyOf": [
|
|
122
|
+
["feature flag", "no flag", "kill switch", "kill-switch", "toggle", "disable", "disabled", "disabling"],
|
|
123
|
+
["rollout", "release", "rollback", "roll back", "deploy", "staged", "store review", "store build", "new build", "revert"]
|
|
124
|
+
],
|
|
125
|
+
"acceptableCoverage": ["E2E", "Integration", "Component"],
|
|
126
|
+
"exampleDescription": "There is no feature flag, so a bad release cannot be turned off without shipping a new store build."
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"unsupportedRisks": [
|
|
130
|
+
{
|
|
131
|
+
"id": "payment-gateway-failure",
|
|
132
|
+
"summary": "A card-processing failure. The epic does not call the billing service and does not change when it charges.",
|
|
133
|
+
"exclusionQuote": "the existing billing service charges the stored card reference on its own nightly schedule, and this epic does not touch it, call it, or change when it runs",
|
|
134
|
+
"anyOf": [
|
|
135
|
+
["payment gateway", "gateway", "charging", "charge fail", "settlement", "card processor", "payment provider"],
|
|
136
|
+
["outage", "unavailable", "declined", "decline", "third-party", "third party", "timeout", "down"]
|
|
137
|
+
],
|
|
138
|
+
"exampleDescription": "The payment gateway is unavailable when the queue uploads, so the customer is never charged for the work."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "cross-tenant-data-leak",
|
|
142
|
+
"summary": "One organization reading another's orders. Each installation has its own database and shares no storage.",
|
|
143
|
+
"exclusionQuote": "one customer organization per installation, with its own database and its own application servers",
|
|
144
|
+
"anyOf": [
|
|
145
|
+
["tenant", "cross-tenant", "multi-tenant", "other organizations", "another organization", "other customers"],
|
|
146
|
+
["leak", "leakage", "isolation", "bleed", "exposure", "boundary", "segregat"]
|
|
147
|
+
],
|
|
148
|
+
"exampleDescription": "A queued order from one organization could be applied to another organization's data, so tenant isolation has to be proven."
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "browser-and-accessibility-regression",
|
|
152
|
+
"summary": "A browser or accessibility regression. The client is a native application with no browser surface.",
|
|
153
|
+
"exclusionQuote": "There is no browser surface and no web view in this epic",
|
|
154
|
+
"anyOf": [
|
|
155
|
+
["accessibility", "a11y", "screen reader", "wcag", "browser", "cross-browser", "safari", "firefox", "responsive layout"]
|
|
156
|
+
],
|
|
157
|
+
"exampleDescription": "The order screen has to stay usable with a screen reader and has to render correctly in every supported browser."
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "data-residency-compliance",
|
|
161
|
+
"summary": "A residency or GDPR exposure. Residency is enforced by a routing layer this epic does not modify.",
|
|
162
|
+
"exclusionQuote": "Data residency is unchanged and is enforced by the platform's regional routing layer, which this epic does not modify.",
|
|
163
|
+
"anyOf": [
|
|
164
|
+
["residency", "gdpr", "sovereignty", "data protection regulation"],
|
|
165
|
+
["compli", "regulat", "jurisdiction", "cross-border", "lawful", "legal"]
|
|
166
|
+
],
|
|
167
|
+
"exampleDescription": "Queued order data could leave its jurisdiction before it is uploaded, which is a GDPR and data residency exposure."
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "clean-last-sync-indicator",
|
|
173
|
+
"root": "clean",
|
|
174
|
+
"projectRoot": "field-sync-indicator",
|
|
175
|
+
"epicNum": "9",
|
|
176
|
+
"epicFile": "docs/epics/epic-9-last-sync-indicator.md",
|
|
177
|
+
"maxRisks": 3,
|
|
178
|
+
"unsupportedRisks": [
|
|
179
|
+
{
|
|
180
|
+
"id": "data-loss-or-corruption",
|
|
181
|
+
"summary": "Losing or corrupting stored data. The change opens the settings store read-only and writes nothing.",
|
|
182
|
+
"exclusionQuote": "The settings store is opened read-only for this field, so no order, queue entry or setting can be altered or lost by it.",
|
|
183
|
+
"anyOf": [
|
|
184
|
+
["corrupt", "data loss", "lose data", "lost", "overwrite", "delete", "truncat"],
|
|
185
|
+
["store", "storage", "record", "field", "database", "setting", "queue"]
|
|
186
|
+
],
|
|
187
|
+
"exampleDescription": "Reading the settings store could corrupt the stored record and drop a queued entry."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "unauthorized-exposure",
|
|
191
|
+
"summary": "Exposing data to someone who should not see it. The value is the device's own timestamp shown to its holder.",
|
|
192
|
+
"exclusionQuote": "it is shown to the person holding the device, so the change exposes nothing to anyone who could not already see it, and it adds no personal data to the screen",
|
|
193
|
+
"anyOf": [
|
|
194
|
+
["unauthori", "leak", "exposure", "expose", "pii", "personal data", "sensitive", "credential"],
|
|
195
|
+
["access", "permission", "auth", "visible", "disclos", "privacy"]
|
|
196
|
+
],
|
|
197
|
+
"exampleDescription": "The sync timestamp could expose sensitive personal data to an unauthorized person with access to the screen."
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": "render-performance-degradation",
|
|
201
|
+
"summary": "A performance regression. One local key lookup on a screen that is already open, with no network call.",
|
|
202
|
+
"exclusionQuote": "The single local key lookup runs on a screen that is already open, so the screen's existing render budget is unchanged.",
|
|
203
|
+
"anyOf": [
|
|
204
|
+
["performance", "slow", "latency", "jank", "frame", "degrad", "throughput", "render budget"],
|
|
205
|
+
["load", "render", "screen", "startup", "budget", "network call", "poll"]
|
|
206
|
+
],
|
|
207
|
+
"exampleDescription": "The extra read could slow the home screen render and degrade startup latency."
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "rollout-without-kill-switch",
|
|
211
|
+
"summary": "An unrecoverable rollout. The row ships behind an existing flag the release process already toggles.",
|
|
212
|
+
"exclusionQuote": "an existing feature flag the release process already toggles per organization, so it can be turned off without a new build",
|
|
213
|
+
"anyOf": [
|
|
214
|
+
["kill switch", "kill-switch", "no feature flag", "cannot be disabled", "unrecoverable", "no way to disable"],
|
|
215
|
+
["rollout", "release", "rollback", "roll back", "deploy", "revert"]
|
|
216
|
+
],
|
|
217
|
+
"exampleDescription": "The new row cannot be disabled without a new build, so a bad rollout has no kill switch."
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Epic 7: Offline order capture for field technicians
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Field technicians work in basements, plant rooms and rural sites where the mobile network
|
|
6
|
+
is unavailable for hours at a time. Today the order screen requires connectivity, so a
|
|
7
|
+
technician either waits for signal or writes the order on paper and re-keys it that evening.
|
|
8
|
+
This epic lets the technician capture and edit an order while offline and have it reach the
|
|
9
|
+
server when connectivity returns.
|
|
10
|
+
|
|
11
|
+
The technician client is a native Android application. There is no browser surface and no
|
|
12
|
+
web view in this epic, and nothing in it changes how the order screen is rendered.
|
|
13
|
+
|
|
14
|
+
The deployment is single-tenant: one customer organization per installation, with its own
|
|
15
|
+
database and its own application servers. No installation can read another installation's
|
|
16
|
+
data, because no storage is shared between them.
|
|
17
|
+
|
|
18
|
+
Data residency is unchanged and is enforced by the platform's regional routing layer, which
|
|
19
|
+
this epic does not modify.
|
|
20
|
+
|
|
21
|
+
Charging is unchanged: the existing billing service charges the stored card reference on its
|
|
22
|
+
own nightly schedule, and this epic does not touch it, call it, or change when it runs.
|
|
23
|
+
|
|
24
|
+
## Stories
|
|
25
|
+
|
|
26
|
+
### 7.1 Capture an order while offline
|
|
27
|
+
|
|
28
|
+
The technician opens an existing work order, adds parts and labour lines, and saves. When the
|
|
29
|
+
device has no connectivity the save writes to a local outbound queue instead of the server,
|
|
30
|
+
and the order screen shows the order as captured.
|
|
31
|
+
|
|
32
|
+
The outbound queue is a plain SQLite file in the application's data directory. It holds the
|
|
33
|
+
full order payload, including the customer's stored card reference and billing address, and it
|
|
34
|
+
is not encrypted at rest. The device's own screen lock is the only thing in front of it.
|
|
35
|
+
|
|
36
|
+
### 7.2 Edit a queued order before it syncs
|
|
37
|
+
|
|
38
|
+
A queued order stays editable. Each edit appends a new entry to the outbound queue rather than
|
|
39
|
+
replacing the earlier one.
|
|
40
|
+
|
|
41
|
+
Two technicians can be assigned to the same work order and can both be offline at once. The
|
|
42
|
+
server applies queued edits in the order they arrive and performs no version check and no
|
|
43
|
+
timestamp check, so a later arrival overwrites an earlier one and the earlier technician's
|
|
44
|
+
lines are lost with no record that they existed.
|
|
45
|
+
|
|
46
|
+
### 7.3 Sync the queue when connectivity returns
|
|
47
|
+
|
|
48
|
+
The client uploads the queue when the operating system reports a usable network.
|
|
49
|
+
|
|
50
|
+
A technician working a full day offline accumulates up to 2,000 queued items. The product
|
|
51
|
+
requires a full backlog of that size to finish syncing within 30 seconds of regaining
|
|
52
|
+
connectivity, on the mid-range devices the field fleet actually carries.
|
|
53
|
+
|
|
54
|
+
A failed upload is retried immediately and indefinitely until the server accepts it. There is
|
|
55
|
+
no backoff, no attempt cap, and no dead-letter path, so a payload the server will never accept
|
|
56
|
+
is uploaded again as fast as the network allows for as long as the application is running.
|
|
57
|
+
|
|
58
|
+
### 7.4 Release
|
|
59
|
+
|
|
60
|
+
The feature ships enabled for every technician in a single release. There is no feature flag,
|
|
61
|
+
no staged rollout, and no way to disable it without shipping a new build through the store
|
|
62
|
+
review queue, which takes between one and three days.
|
|
63
|
+
|
|
64
|
+
## Acceptance criteria
|
|
65
|
+
|
|
66
|
+
- A technician with no connectivity can save an order and see it listed as captured.
|
|
67
|
+
- A captured order remains editable until it has been accepted by the server.
|
|
68
|
+
- The queue uploads without the technician taking any action once the network returns.
|
|
69
|
+
- An order that reached the server appears on the dispatcher's web console unchanged.
|
|
70
|
+
|
|
71
|
+
## Out of scope
|
|
72
|
+
|
|
73
|
+
- Offline capture of anything other than work orders.
|
|
74
|
+
- Any change to the dispatcher's web console.
|
|
75
|
+
- Any change to how or when a card is charged.
|