@1aboveio/skills 0.20.1 → 0.20.3
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 +12 -5
- package/package.json +1 -1
- package/runtime/skills/distribution/generated/recipes.json +220 -25
- package/runtime/skills/distribution/scripts/bundles.mjs +49 -5
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +4 -0
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
- package/skills/cicd-pipeline/cloud-build/SKILL.md +6 -6
- package/skills/compliance/compliance-audit/LICENSE +3 -0
- package/skills/compliance/compliance-audit/SKILL.md +466 -0
- package/skills/compliance/compliance-audit/references/findings-schema.md +290 -0
- package/skills/compliance/compliance-audit/references/vars-checklist.md +1068 -0
- package/skills/compliance/compliance-audit/references/vars-findings-schema.md +395 -0
- package/skills/compliance/compliance-audit/scripts/reconcile_tracker.py +454 -0
- package/skills/compliance/compliance-audit/scripts/test_validate_tracker.py +90 -0
- package/skills/compliance/compliance-audit/scripts/validate_tracker.py +752 -0
- package/skills/compliance/doc-authenticity/LICENSE +3 -0
- package/skills/compliance/doc-authenticity/SKILL.md +158 -0
- package/skills/compliance/doc-authenticity/references/agreement.md +66 -0
- package/skills/compliance/doc-authenticity/references/bank-statement.md +150 -0
- package/skills/compliance/doc-authenticity/references/forensic-signals.md +157 -0
- package/skills/compliance/doc-authenticity/references/image-signals.md +114 -0
- package/skills/compliance/doc-authenticity/references/invoice.md +66 -0
- package/skills/compliance/doc-authenticity/references/issuer-profiles.md +86 -0
- package/skills/compliance/doc-authenticity/references/verdict-and-report.md +95 -0
- package/skills/compliance/doc-authenticity/references/verdict.schema.json +81 -0
- package/skills/compliance/doc-authenticity/scripts/image_forensics.py +294 -0
- package/skills/compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
- package/skills/compliance/doc-authenticity/scripts/validate_codes.py +224 -0
- package/skills/compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
- package/skills/compliance/report-writing/LICENSE +3 -0
- package/skills/compliance/report-writing/SKILL.md +206 -0
- package/skills/compliance/report-writing/references/audit-report-template.md +121 -0
- package/skills/compliance/report-writing/references/external-assessment-template.md +167 -0
- package/skills/compliance/report-writing/references/management-report-template.md +145 -0
- package/skills/compliance/report-writing/references/vars-report-template.md +224 -0
- package/skills/document-utils/doc-to-md/LICENSE +3 -0
- package/skills/document-utils/doc-to-md/SKILL.md +77 -0
- package/skills/document-utils/doc-to-md/scripts/convert.py +157 -0
- package/skills/document-utils/reg-parser/LICENSE +3 -0
- package/skills/document-utils/reg-parser/SKILL.md +201 -0
- package/skills/document-utils/reg-parser/references/checklist-schema.md +223 -0
- package/skills/document-utils/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
- package/skills/document-utils/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
- package/skills/document-utils/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
- package/skills/document-utils/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
- package/skills/document-utils/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
- package/skills/document-utils/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
- package/skills/document-utils/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
- package/skills/document-utils/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
- package/skills/document-utils/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
- package/skills/document-utils/reg-parser/references/mandates/visa-vars.md +205 -0
- package/skills/document-utils/reg-parser/scripts/compare_checklist.py +228 -0
- package/skills/document-utils/reg-parser/scripts/dump_checklist.py +348 -0
- package/skills/document-utils/reg-parser/scripts/preprocess-mandate.sh +171 -0
- package/skills/document-utils/reg-parser/scripts/test_quarantine.sh +102 -0
- package/skills/document-utils/reg-parser/scripts/validate_checklist.py +532 -0
- package/skills/document-utils/reg-parser/scripts/validate_mandate.py +242 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +17 -17
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +4 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
- package/skills/engineering/rush-release/SKILL.md +46 -25
- package/skills/engineering/rush-release/references/cut.md +21 -4
- package/skills/engineering/rush-release/references/preflight.md +25 -6
- package/skills/engineering/rush-release/references/promotion.md +70 -0
- package/skills/engineering/rush-release/references/publish.md +49 -16
- package/skills/engineering/rush-release/scripts/plan.mjs +33 -7
- package/skills/engineering/smoke/SKILL.md +4 -4
- package/skills/engineering/smoke/references/manifest.md +3 -3
- package/skills/payment/similar-domain/LICENSE +3 -0
- package/skills/payment/similar-domain/SKILL.md +178 -0
- package/skills/payment/similar-domain/references/entities.md +58 -0
- package/skills/payment/similar-domain/references/fingerprints.md +82 -0
- package/skills/payment/similar-domain/references/pivots.md +66 -0
- package/skills/payment/similar-domain/references/report-template.md +76 -0
- package/skills/payment/similar-domain/references/scoring.md +61 -0
- package/skills/payment/similar-domain/references/terminology.md +37 -0
- package/skills/payment/similar-domain/references/workflow.md +79 -0
- package/skills/payment/tld-detection/SKILL.md +76 -21
- package/skills/payment/tld-detection/references/clean.md +1 -1
- package/skills/payment/tld-detection/references/clustering.md +21 -6
- package/skills/payment/tld-detection/references/decision.md +7 -2
- package/skills/payment/tld-detection/references/fx-conversion.md +229 -0
- package/skills/payment/tld-detection/references/report-template.md +63 -27
- package/skills/payment/tld-detection/references/streams.md +90 -0
- package/skills/payment/tld-detection/references/terminology.md +15 -7
- package/skills/payment/tld-detection/references/workflow.md +1 -1
- package/skills/payment/tld-detection/scripts/detect_streams.py +542 -0
- package/skills/engineering/ensure-coverage/evals/evals.json +0 -125
- package/skills/engineering/rush-issues/evals/evals.json +0 -65
- package/skills/engineering/rush-release/evals/evals.json +0 -44
- package/skills/fullstack/shadcn/evals/evals.json +0 -90
- package/skills/payment/fraud-analysis/evals/evals.json +0 -40
- package/skills/payment/tld-detection/evals/evals.json +0 -107
- package/skills/payment/tld-detection/tests/test_detect_ladders.py +0 -132
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Cloud Run staged promotion
|
|
2
|
+
|
|
3
|
+
Promote one immutable candidate from 0% to 100% under the complete-plan
|
|
4
|
+
authorization. Load `smoke` for both validation profiles and the applicable
|
|
5
|
+
platform skill for repository-declared Cloud Run commands. Do not restore the
|
|
6
|
+
archived `resolve-release` state machine or introduce another confirmation.
|
|
7
|
+
|
|
8
|
+
## 1. Bind the candidate
|
|
9
|
+
|
|
10
|
+
After the RC-tag build reports `SUCCESS`, read its durable candidate receipt.
|
|
11
|
+
For every affected service, independently verify from one fresh Cloud Run
|
|
12
|
+
snapshot that:
|
|
13
|
+
|
|
14
|
+
- receipt RC tag and source SHA equal the approved plan and `<mergedSha>`;
|
|
15
|
+
- the named immutable revision runs the receipt's image digest;
|
|
16
|
+
- the candidate URL resolves to that revision;
|
|
17
|
+
- the candidate has exactly 0% traffic and the recorded incumbent exactly 100%;
|
|
18
|
+
- no second release or unknown traffic-bearing revision is in flight.
|
|
19
|
+
|
|
20
|
+
Missing, duplicate, stale, or contradictory identity is a pre-exposure failure.
|
|
21
|
+
Delete the candidate by immutable revision only when the preflight contract
|
|
22
|
+
declared that deletion safe; otherwise leave it at 0% and hand back. Never move
|
|
23
|
+
the RC tag.
|
|
24
|
+
|
|
25
|
+
## 2. Validate at 0%
|
|
26
|
+
|
|
27
|
+
Run the repository's smoke workflow against the exact candidate URL with
|
|
28
|
+
`--profile prod-preview`. Accept only a non-empty `ALIVE` verdict whose stamped
|
|
29
|
+
target is that URL, profile is `prod-preview`, and `noMutations` is `true`.
|
|
30
|
+
Record its `generatedAt`. A failed or unavailable gate leaves production on the
|
|
31
|
+
incumbent and does not consume the final tag.
|
|
32
|
+
|
|
33
|
+
## 3. Recheck and shift
|
|
34
|
+
|
|
35
|
+
Immediately before exposure, re-read candidate identity and traffic. Require
|
|
36
|
+
the candidate still at 0%, incumbent still at 100%, and the ALIVE verdict still
|
|
37
|
+
within the plan's declared freshness window. If durable processing is affected,
|
|
38
|
+
run the preflight-declared pause order and prove quiescence before traffic moves.
|
|
39
|
+
|
|
40
|
+
Shift all affected services to the candidate in the declared dependency order.
|
|
41
|
+
After each command, read traffic back. If a later service cannot shift, restore
|
|
42
|
+
already-shifted services in reverse order before doing anything else. The plan's
|
|
43
|
+
single confirmation already authorized this conditional shift and restoration.
|
|
44
|
+
|
|
45
|
+
## 4. Verify live
|
|
46
|
+
|
|
47
|
+
From fresh control-plane and service observations, require for every service:
|
|
48
|
+
|
|
49
|
+
- the exact candidate revision holds 100% and the incumbent 0%;
|
|
50
|
+
- serving source SHA and image digest equal the candidate receipt;
|
|
51
|
+
- the live URL passes a non-empty, read-only `prod-active` smoke as `ALIVE`.
|
|
52
|
+
|
|
53
|
+
Resume durable processing only against the verified serving candidate, then
|
|
54
|
+
prove scheduling/admission and any required publication advancement are healthy.
|
|
55
|
+
Only this state permits the final release tag.
|
|
56
|
+
|
|
57
|
+
## 5. Failure routing
|
|
58
|
+
|
|
59
|
+
- **Before any traffic moved:** production is untouched. Delete or retain the
|
|
60
|
+
0% candidate according to the recorded policy and hand back.
|
|
61
|
+
- **After a known failed shift or live check:** automatically restore every
|
|
62
|
+
service to its recorded incumbent in reverse dependency order, restore
|
|
63
|
+
durable processing against that incumbent, and verify incumbent 100%,
|
|
64
|
+
candidate 0%, and `prod-active` ALIVE before hand-back.
|
|
65
|
+
- **Ambiguous traffic, identity, or processing state:** stop all writes and hand
|
|
66
|
+
back the last known-good evidence for operator recovery. Never guess, retry a
|
|
67
|
+
shift, delete a revision, or claim rollback succeeded.
|
|
68
|
+
|
|
69
|
+
The final tag remains unused on every failure path. An RC tag is immutable and
|
|
70
|
+
remains as attempt evidence.
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# Publish
|
|
2
2
|
|
|
3
|
-
Push the cut, tag it, and watch the publisher.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Push the cut, tag it, and watch the publisher. A pushed final tag that fails to
|
|
4
|
+
publish **burns the version**. A Cloud Run RC tag that fails burns that RC
|
|
5
|
+
ordinal, not the final version. Do not move or delete either tag to "reuse" it.
|
|
6
|
+
|
|
7
|
+
The complete-plan confirmation in [cut.md](cut.md) already authorizes every
|
|
8
|
+
action in this procedure and [promotion.md](promotion.md). Do not insert another
|
|
9
|
+
confirmation before the branch push, PR queue, candidate validation, traffic
|
|
10
|
+
shift, rollback, tag push, GitHub Release, or publisher watch. A provider or
|
|
11
|
+
repository gate either passes and the sequence continues, or fails and the run
|
|
12
|
+
hands back. If resolving a failure would change the approved identity or action
|
|
13
|
+
set, return to planning and ask once on the replacement plan.
|
|
6
14
|
|
|
7
15
|
## 1. Push and merge the metadata PR
|
|
8
16
|
|
|
@@ -27,7 +35,7 @@ documented release path, a successful fast-forward of `<cutSha>` to `main`
|
|
|
27
35
|
may use `<cutSha>` as `<mergedSha>`. Never infer this from local state: re-fetch
|
|
28
36
|
and verify the remote ref.
|
|
29
37
|
|
|
30
|
-
## 2. Verify
|
|
38
|
+
## 2. Verify the merged main SHA
|
|
31
39
|
|
|
32
40
|
The release tag must name a commit reachable from the remote default branch.
|
|
33
41
|
This is the ancestry gate used by release publishers to refuse off-main
|
|
@@ -40,8 +48,6 @@ git merge-base --is-ancestor "<mergedSha>" origin/main
|
|
|
40
48
|
test "$(git rev-parse "<cutSha>^{tree}")" = \
|
|
41
49
|
"$(git rev-parse "<mergedSha>^{tree}")"
|
|
42
50
|
test "$(git show "<mergedSha>:package.json" | jq -r .version)" = "<nextVersion>"
|
|
43
|
-
git tag -a "<tag>" "<mergedSha>" -m "Release <tag>"
|
|
44
|
-
git push --no-verify origin "refs/tags/<tag>"
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
The tree equality check proves the provider merge introduced no post-freeze
|
|
@@ -52,6 +58,30 @@ when it is not `package.json`. Re-read the exact required queue/CI result for
|
|
|
52
58
|
`<mergedSha>` before tagging. A green cut head is not evidence for a different
|
|
53
59
|
merge commit.
|
|
54
60
|
|
|
61
|
+
## 3. Publish by mode
|
|
62
|
+
|
|
63
|
+
**Artifact-only:** create and push the final annotated tag on `<mergedSha>`,
|
|
64
|
+
then continue to the publisher watch:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git tag -a "<tag>" "<mergedSha>" -m "Release <tag>"
|
|
68
|
+
git push --no-verify origin "refs/tags/<tag>"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Cloud Run staged:** create and push `<candidateTag>` on `<mergedSha>`. Watch
|
|
72
|
+
Cloud Build until it emits the exact candidate receipt, then execute
|
|
73
|
+
[promotion.md](promotion.md). Only after live verification succeeds, create and
|
|
74
|
+
push the final tag on the same `<mergedSha>`. The final tag must not trigger a
|
|
75
|
+
second Cloud Run build.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git tag -a "<candidateTag>" "<mergedSha>" -m "Candidate <candidateTag>"
|
|
79
|
+
git push --no-verify origin "refs/tags/<candidateTag>"
|
|
80
|
+
# After promotion.md succeeds:
|
|
81
|
+
git tag -a "<tag>" "<mergedSha>" -m "Release <tag>"
|
|
82
|
+
git push --no-verify origin "refs/tags/<tag>"
|
|
83
|
+
```
|
|
84
|
+
|
|
55
85
|
`--no-verify` is for tag refs, because a Mergify (or similar) pre-push hook
|
|
56
86
|
that rewrites `git push` into a stack push is correct for branches and wrong
|
|
57
87
|
for immutable tags.
|
|
@@ -64,19 +94,21 @@ If preflight recorded a workflow that publishes only on
|
|
|
64
94
|
remote (`gh release create <tag> --notes-file …`). Otherwise the tag push is
|
|
65
95
|
the trigger.
|
|
66
96
|
|
|
67
|
-
##
|
|
97
|
+
## 4. Watch
|
|
68
98
|
|
|
69
99
|
Launch one background watcher; do not poll in the foreground.
|
|
70
100
|
|
|
71
|
-
**Cloud Build** (`publisher` `cloudbuild` or `both`)
|
|
101
|
+
**Cloud Build** (`publisher` `cloudbuild` or `both`) is watched first for the RC
|
|
102
|
+
tag and exact merged SHA. `SUCCESS` means only that candidate production
|
|
103
|
+
succeeded; Cloud Run success requires [promotion.md](promotion.md):
|
|
72
104
|
|
|
73
105
|
```bash
|
|
74
106
|
gcloud builds list --limit=1 --format='value(id,status,substitutions.TAG_NAME)' \
|
|
75
|
-
--filter="substitutions.TAG_NAME=<
|
|
107
|
+
--filter="substitutions.TAG_NAME=<candidateTag> OR tags=<candidateTag>"
|
|
76
108
|
```
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
`FAILURE` / `TIMEOUT` / `CANCELLED` is a hand-back. Load `cloud-debug` only to
|
|
111
|
+
diagnose, not to rerun the release.
|
|
80
112
|
|
|
81
113
|
**npm** (`publisher` `npm` or `both`):
|
|
82
114
|
|
|
@@ -92,9 +124,10 @@ When a GitHub Actions workflow is the npm publisher, `gh run list` /
|
|
|
92
124
|
`gh run watch` for the tag SHA is the wait; `npm view` remains the proof the
|
|
93
125
|
artifact exists.
|
|
94
126
|
|
|
95
|
-
##
|
|
127
|
+
## 5. Failure
|
|
96
128
|
|
|
97
|
-
If
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
If an artifact watch fails after the final tag, leave it in place and hand
|
|
130
|
+
back that the version is burned. If a staged candidate build fails, leave its
|
|
131
|
+
RC tag, keep the final tag unused, and hand back with production untouched.
|
|
132
|
+
Promotion failures follow [promotion.md](promotion.md). A fix starts from a new
|
|
133
|
+
green HEAD and a replacement plan; tags are never moved or deleted.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Changelog + SemVer plan from baseline tag..SHA.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// node plan.mjs --sha <sha> [--trunk main] [--json] [--strict]
|
|
5
|
+
// node plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]
|
|
6
6
|
//
|
|
7
7
|
// Exit: 0 ok · 1 git/inspect failed · 2 usage
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs'
|
|
@@ -50,6 +50,16 @@ export function latestReleaseTag(tags) {
|
|
|
50
50
|
return parsed[parsed.length - 1]
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export function nextReleaseCandidateTag(finalTag, tags) {
|
|
54
|
+
const escaped = finalTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
55
|
+
const pattern = new RegExp(`^${escaped}-rc\\.([1-9]\\d*)$`)
|
|
56
|
+
const ordinals = tags
|
|
57
|
+
.map((tag) => pattern.exec(tag))
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((match) => Number(match[1]))
|
|
60
|
+
return `${finalTag}-rc.${ordinals.length ? Math.max(...ordinals) + 1 : 1}`
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export function classifySubject(subject, body = '') {
|
|
54
64
|
const title = String(subject || '').trim()
|
|
55
65
|
const match = TITLE_RE.exec(title)
|
|
@@ -207,9 +217,16 @@ function parseLog(stdout) {
|
|
|
207
217
|
})
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
export function planRelease({
|
|
220
|
+
export function planRelease({
|
|
221
|
+
sha,
|
|
222
|
+
trunk = 'main',
|
|
223
|
+
strict = false,
|
|
224
|
+
cloudRun = false,
|
|
225
|
+
cwd = process.cwd(),
|
|
226
|
+
now = new Date(),
|
|
227
|
+
} = {}) {
|
|
211
228
|
if (!sha) {
|
|
212
|
-
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict]')
|
|
229
|
+
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]')
|
|
213
230
|
err.exitCode = 2
|
|
214
231
|
throw err
|
|
215
232
|
}
|
|
@@ -230,6 +247,13 @@ export function planRelease({ sha, trunk = 'main', strict = false, cwd = process
|
|
|
230
247
|
const { bump, reason } = bumpFor(changes, current, { strict })
|
|
231
248
|
const next = nextVersion(current, bump)
|
|
232
249
|
const version = formatVersion(next)
|
|
250
|
+
const tag = `v${version}`
|
|
251
|
+
const candidateTag = cloudRun
|
|
252
|
+
? nextReleaseCandidateTag(tag, git(['tag', '--list', `${tag}-rc.*`], cwd)
|
|
253
|
+
.split('\n')
|
|
254
|
+
.map((line) => line.trim())
|
|
255
|
+
.filter(Boolean))
|
|
256
|
+
: null
|
|
233
257
|
const date = now.toISOString().slice(0, 10)
|
|
234
258
|
return {
|
|
235
259
|
trunk,
|
|
@@ -239,7 +263,8 @@ export function planRelease({ sha, trunk = 'main', strict = false, cwd = process
|
|
|
239
263
|
bump,
|
|
240
264
|
reason,
|
|
241
265
|
nextVersion: version,
|
|
242
|
-
tag
|
|
266
|
+
tag,
|
|
267
|
+
candidateTag,
|
|
243
268
|
versionFiles: files,
|
|
244
269
|
changes,
|
|
245
270
|
date,
|
|
@@ -248,11 +273,12 @@ export function planRelease({ sha, trunk = 'main', strict = false, cwd = process
|
|
|
248
273
|
}
|
|
249
274
|
|
|
250
275
|
function parseArgs(argv) {
|
|
251
|
-
const opts = { json: false, strict: false, trunk: 'main', sha: null }
|
|
276
|
+
const opts = { json: false, strict: false, cloudRun: false, trunk: 'main', sha: null }
|
|
252
277
|
for (let i = 0; i < argv.length; i += 1) {
|
|
253
278
|
const arg = argv[i]
|
|
254
279
|
if (arg === '--json') opts.json = true
|
|
255
280
|
else if (arg === '--strict') opts.strict = true
|
|
281
|
+
else if (arg === '--cloud-run') opts.cloudRun = true
|
|
256
282
|
else if (arg === '--sha') opts.sha = argv[++i]
|
|
257
283
|
else if (arg === '--trunk') opts.trunk = argv[++i]
|
|
258
284
|
else if (arg === '--help' || arg === '-h') opts.help = true
|
|
@@ -264,7 +290,7 @@ function parseArgs(argv) {
|
|
|
264
290
|
}
|
|
265
291
|
if (opts.help) return opts
|
|
266
292
|
if (!opts.sha || !opts.trunk) {
|
|
267
|
-
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict]')
|
|
293
|
+
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]')
|
|
268
294
|
err.exitCode = 2
|
|
269
295
|
throw err
|
|
270
296
|
}
|
|
@@ -274,7 +300,7 @@ function parseArgs(argv) {
|
|
|
274
300
|
export function main(argv = process.argv.slice(2)) {
|
|
275
301
|
const opts = parseArgs(argv)
|
|
276
302
|
if (opts.help) {
|
|
277
|
-
process.stdout.write('plan.mjs --sha <sha> [--trunk main] [--json] [--strict]\n')
|
|
303
|
+
process.stdout.write('plan.mjs --sha <sha> [--trunk main] [--json] [--strict] [--cloud-run]\n')
|
|
278
304
|
return 0
|
|
279
305
|
}
|
|
280
306
|
const result = planRelease(opts)
|
|
@@ -63,7 +63,7 @@ node <smoke>/scripts/smoke.mjs plan --manifest smoke.manifest.json --profile pro
|
|
|
63
63
|
|
|
64
64
|
`--target` is now **just the address to drive against** (the deployed candidate/service URL; defaults to `local`) — it no longer participates in selection. The profile does. For a `local` profile, boot the build yourself the `/verify` way (a `run-*`/`verifier-*` skill in `.claude/skills/`, else the **`run`** skill, else cold-start from README/package.json, timebox ~15 min → BLOCKED); for a deployed profile, point `--target` at the revision and confirm it is the SHA you mean to verify.
|
|
65
65
|
|
|
66
|
-
**Prod safety is mechanical, and it lives on the profile.** A path runs in a profile only if it declares that profile AND the profile's `mutationPolicy` admits it. The **production profiles (`prod-preview`, `prod-active`) are read-only** — every `mutates:true` path is excluded, **tenant or not** (a synthetic tenant still writes to the real production database, and a revision at 0% traffic is not a reason those writes are safe
|
|
66
|
+
**Prod safety is mechanical, and it lives on the profile.** A path runs in a profile only if it declares that profile AND the profile's `mutationPolicy` admits it. The **production profiles (`prod-preview`, `prod-active`) are read-only** — every `mutates:true` path is excluded, **tenant or not** (a synthetic tenant still writes to the real production database, and a revision at 0% traffic is not a reason those writes are safe). `dev-active` allows a `mutates:true` path **only with a declared synthetic tenant**; the local profiles allow isolated writes. This is the old `--no-mutations` flag turned into a property of the *named profile*, so nobody has to remember to pass it — and because one name drives both the run and the verdict, the two **cannot disagree** about the selected set (the old run/verdict flag-mismatch is unrepresentable).
|
|
67
67
|
|
|
68
68
|
**Gate before promote.** On a deployed target the smoke is a gate only if it runs *before* the revision is irreversible — a smoke after traffic shifts is a monitor, not a gate. Deploy the candidate at **0% traffic** (stable, addressable) → smoke that URL under `prod-preview` → **promote only on ALIVE** → confirm the serving revision is the one you smoked, then re-smoke the live service under `prod-active`. Bonus: a DEAD is re-investigable against the same 0%-traffic revision with no rebuild — most of what a deployed smoke catches is config/secret/packaging. **Reuse the platform's deploy notification** (the CI/deploy build-failure → Pub/Sub/webhook path), carrying `smoke-verdict.json` so the alert names the broken path; the concrete placement lives in `cloud-build` / `cloud-deploy`.
|
|
69
69
|
|
|
@@ -82,7 +82,7 @@ node <smoke>/scripts/smoke.mjs verdict --manifest smoke.manifest.json --profile
|
|
|
82
82
|
```
|
|
83
83
|
**ALIVE** iff every selected path PASSED. Any `fail`, `blocked`, or `missing` → **DEAD**, naming the path(s). Exit code is the gate (0 = ALIVE, 1 = DEAD). Selection is a pure function of `--profile` + the manifest, so the run and the verdict select the **same** set by construction — there is no second flag assembly for the verdict to disagree with, which is the whole point: the run/verdict mismatch that free-form targeting permitted is now unrepresentable.
|
|
84
84
|
|
|
85
|
-
`smoke-verdict.json` is stamped with the facts an automated consumer needs to judge the evidence rather than trust it: **`profile`** (the Validation Profile it selected under), **`target`** (which URL it ran against), **`noMutations`** (DERIVED from the profile's `mutationPolicy` — `true` for the read-only production profiles), and **`generatedAt`** (an ISO-8601 instant — *when*, so a consumer can bound how old the evidence is). `
|
|
85
|
+
`smoke-verdict.json` is stamped with the facts an automated consumer needs to judge the evidence rather than trust it: **`profile`** (the Validation Profile it selected under), **`target`** (which URL it ran against), **`noMutations`** (DERIVED from the profile's `mutationPolicy` — `true` for the read-only production profiles), and **`generatedAt`** (an ISO-8601 instant — *when*, so a consumer can bound how old the evidence is). `rush-release`'s traffic-shift gate compares these; an ALIVE from the wrong target, a mutating run, or too long ago is not a green light.
|
|
86
86
|
|
|
87
87
|
**`generatedAt` dates the verdict, not the run.** `verdict` reduces whatever `--results` file you hand it, however old, and stamps the current instant on the result — so re-running `verdict` alone re-mints freshness without re-validating anything, and a consumer bounding the age cannot tell the difference. That is not a substitute for a stale gate's re-check: when what expired is the *evidence about a live target*, re-drive the paths into a new `smoke-results.json` — re-minting a verdict is not re-validation.
|
|
88
88
|
|
|
@@ -113,8 +113,8 @@ The **per-PR golden-path liveness floor** (`ensure-coverage` CI contract, gate 8
|
|
|
113
113
|
- **CI (gate 8, blocking)** — wired in `e2e-test/assets/ci-gates.*`; boots the app, drives the paths into `smoke-results.json`, runs `smoke.mjs verdict`. The mechanical backstop.
|
|
114
114
|
- **`resolve-issues`** — final step of the [epic integration gate](../resolve-issues/references/deliverables.md#deliverables-the-shippable-component-not-the-whole-epic): boot the assembled integration branch and run the golden paths; a DEAD whose broken paths are `fail` belongs to a specific unit → reopen its fix loop. **A DEAD built from `blocked`/`missing` paths is a gate-environment problem, not a unit's defect** — report it and fix the environment; reopening a fix loop on it spends a producer spawn and an adversarial review on code that is fine, and the round lands in the unit's budget all the same.
|
|
115
115
|
- **`/smoke`** — standalone, any time, against `local` or a URL.
|
|
116
|
-
-
|
|
117
|
-
- **The dev-deploy recorder** (`cloud-build` → Post-deploy smoke gate) — the integration pipeline runs this gate after its 100%-traffic deploy and publishes the verdict as a **SHA-keyed commit status**, never failing the build; that recorded ALIVE
|
|
116
|
+
- **`rush-release` Cloud Run flow** — the `prod-preview` profile against the 0%-traffic candidate URL (read-only, so it excludes every mutating path), then the `prod-active` profile against the live service after the shift. A DEAD **before** exposure leaves production untouched; a DEAD after it triggers the recorded rollback policy.
|
|
117
|
+
- **The dev-deploy recorder** (`cloud-build` → Post-deploy smoke gate) — the integration pipeline runs this gate after its 100%-traffic deploy and publishes the verdict as a **SHA-keyed commit status**, never failing the build; when configured as required CI, that recorded ALIVE helps `rush-release` select its green trunk SHA.
|
|
118
118
|
|
|
119
119
|
## Reference Map
|
|
120
120
|
|
|
@@ -82,7 +82,7 @@ JSON (`smoke.manifest.json`). A repo may author in YAML and project to JSON —
|
|
|
82
82
|
| `command` | cli | the command to run, for `cli` paths (alternative to `start`) |
|
|
83
83
|
| `observable` | api/cli | the **concrete thing that proves the path worked** — the 200 body, the exit code. Required for `api`/`cli` (e2e-test doesn't cover them); for `browser` the referenced journey's assertions *are* the observable. |
|
|
84
84
|
| `intent` | no | optional one-line human summary of what the browser journey proves — documentation only, not enforced. |
|
|
85
|
-
| `mutates` | no | `true` if the path writes durable state. Forces a `tenant` and gates it out of read-only profiles. Under a **read-only profile** (any production profile), a tenant grants nothing: every `mutates:true` path is excluded regardless
|
|
85
|
+
| `mutates` | no | `true` if the path writes durable state. Forces a `tenant` and gates it out of read-only profiles. Under a **read-only profile** (any production profile), a tenant grants nothing: every `mutates:true` path is excluded regardless. |
|
|
86
86
|
| `tenant` | when `mutates` | a **synthetic/test tenant** id so the mutating path can run under a `synthetic-tenant` profile (e.g. `dev-active`) without touching real data. A `mutates:true` path with no `tenant` is excluded from a `synthetic-tenant` profile. This is **not** a production permit: the production database is real whether or not the revision serves traffic, so the read-only production profiles exclude the path either way. Name it with the **`e2e-` prefix** (`e2e-synthetic`) per the e2e-* account convention (`e2e-test` → `references/authoring/auth-flows.md`) — `validate` warns on an unprefixed tenant, because that's the one mistaken for live data. |
|
|
87
87
|
| `profiles` | **when a `profiles` registry exists** | the [Validation Profiles](#validation-profiles-the-profiles-registry) this path is required in — a non-empty array of names from the registry. A path is only ever selected in a profile it declares; a path that declares none is a silent non-runner, so `validate` **fails closed** on it once a registry is present. (A per-path array, rather than the inverse profile→paths mapping, so a path's selection facts — `mutates`, `tenant`, `profiles` — all sit together on the path they govern.) |
|
|
88
88
|
|
|
@@ -123,11 +123,11 @@ Read-only paths always run in any profile they declare.
|
|
|
123
123
|
| `prod-preview` | `pre-promotion` | `prod` | `preview` | `read-only` |
|
|
124
124
|
| `prod-active` | `post-promotion` | `prod` | `active` | `read-only` |
|
|
125
125
|
|
|
126
|
-
`
|
|
126
|
+
`rush-release` consumes `prod-preview` before shifting the candidate from 0% traffic and `prod-active` after the shift. Each smoke **path** declares the profiles it is required in (`"profiles": ["pr-local", "prod-preview"]`); a path selected by zero eligible paths under a profile yields the existing **EMPTY** verdict — a gate that proves nothing must not look like a pass.
|
|
127
127
|
|
|
128
128
|
**Recommended, not mandatory (for now).** A manifest with no `profiles` registry still `validate`s (legacy manifests don't break) but `validate` warns, and `plan`/`verdict --profile` cannot run against it. The moment a registry is present it is fully checked, and every path must declare its profiles.
|
|
129
129
|
|
|
130
|
-
**Reserved names are pinned to their gate.** The five shipped names are consumed by `
|
|
130
|
+
**Reserved names are pinned to their gate.** The five shipped names are consumed by `rush-release` (and the other lanes) by **hardcoded string** — staged promotion drives `--profile prod-preview` before exposure and `prod-active` after — so the name is itself a safety key, not just a label. `validate` therefore **pins each reserved name to its gate**: `pr-local`→`pull-request`, `queue-local`→`merge-queue`, `dev-active`→`trunk-integration`, `prod-preview`→`pre-promotion`, `prod-active`→`post-promotion`. From the gate, the `(environment, endpoint)` table and the prod-read-only rule force the rest — so an entry *named* `prod-preview` but *bound* to a writable dev gate **fails closed**, instead of letting `plan --profile prod-preview` inherit a `synthetic-tenant` write policy against the production candidate.
|
|
131
131
|
|
|
132
132
|
| Hazard | Failure mode | What to prove |
|
|
133
133
|
|---|---|---|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: similar-domain
|
|
3
|
+
description: >
|
|
4
|
+
Find the domains, mirrors, and operating entities related to one or more seed
|
|
5
|
+
domains: collect technical fingerprints (analytics IDs, block-page templates,
|
|
6
|
+
DNS, mail, certificates, registration), reverse-pivot on each fingerprint,
|
|
7
|
+
score every link, and expand until the cluster stops growing. Use whenever
|
|
8
|
+
the user gives a domain or URL and asks who is behind it, whether several
|
|
9
|
+
sites are related or the same operator, for sister sites, mirror domains,
|
|
10
|
+
the cluster, or a merchant-website background check — even if they never say
|
|
11
|
+
similar-domain. Stops at websites, mirrors and operators: anything inside
|
|
12
|
+
transaction data (streams, preset menus, amount ladders) is tld-detection,
|
|
13
|
+
and transaction-level fraud rules are fraud-analysis.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Similar Domain
|
|
17
|
+
|
|
18
|
+
Start from a seed domain and find every other domain that the same people run.
|
|
19
|
+
A cluster is defined by **shared infrastructure**, not by shared branding or
|
|
20
|
+
shared industry. Corporate records (operator, licence, affiliate program)
|
|
21
|
+
confirm and name the cluster; they do not create it on their own.
|
|
22
|
+
|
|
23
|
+
The method is a loop: read fingerprints off a domain, ask "who else has this?"
|
|
24
|
+
for each fingerprint, score what comes back, and feed every new domain back
|
|
25
|
+
into the loop.
|
|
26
|
+
|
|
27
|
+
**Produce:** a scored cluster with an evidence matrix, the entity chain behind
|
|
28
|
+
it, and a ≤3-sentence verdict, under `reports/similar-domain/`.
|
|
29
|
+
**Do not produce:** an ownership claim with no corporate evidence, a cluster
|
|
30
|
+
built from one weak signal, or a list of every site on the same CDN.
|
|
31
|
+
|
|
32
|
+
Paths under `references/` are relative to this skill.
|
|
33
|
+
|
|
34
|
+
## Workflow
|
|
35
|
+
|
|
36
|
+
Follow these steps in order. Detail:
|
|
37
|
+
[references/workflow.md](references/workflow.md).
|
|
38
|
+
|
|
39
|
+
| Step | Do | Gate |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 0 Pre-flight | Seeds, question, jurisdiction, access limits | Stop if there is no resolvable domain; note geo-blocks and challenge pages |
|
|
42
|
+
| 1 Fingerprint | Record every non-brand identifier on each domain | Every fingerprint has a source and a timestamp; local resolver artefacts are flagged |
|
|
43
|
+
| 2 Pivot | Reverse-search each fingerprint for other domains | Each pivot is logged with source class and hit count; high-noise pivots are filtered by a second fingerprint |
|
|
44
|
+
| 3 Expand names | Enumerate mirror and numbering variants of every brand found | Candidates are only kept if they share a fingerprint from step 1 |
|
|
45
|
+
| 4 Entity | Operator, licence, address, affiliate program, terms page | Each entity fact cites a page; conflicting operator names are kept, not resolved by guess |
|
|
46
|
+
| 5 Score | Weight every link; label each domain | Two independent strong signals for Confirmed; one for Probable; nothing weaker than Possible enters the cluster table |
|
|
47
|
+
| 6 Loop | Send every new Confirmed or Probable domain back to step 1 | Stop when a full pass adds no new domain |
|
|
48
|
+
| 7 Report | Verdict, cluster table, evidence matrix, entity chain, outliers, pivot log | ≤3-sentence verdict; no ownership wording without an entity citation |
|
|
49
|
+
|
|
50
|
+
### 0 — Pre-flight
|
|
51
|
+
|
|
52
|
+
Record the seed domains, what the user actually wants to know (same
|
|
53
|
+
operator? mirrors? merchant background?), and any jurisdiction or time limits.
|
|
54
|
+
Note what you cannot see: geo-blocks, bot challenges, private WHOIS. A blocked
|
|
55
|
+
site still yields a block page, DNS, certificates, and history, so blocking is
|
|
56
|
+
a finding, not a dead end. Grain and limits:
|
|
57
|
+
[workflow.md](references/workflow.md).
|
|
58
|
+
|
|
59
|
+
### 1 — Fingerprint
|
|
60
|
+
|
|
61
|
+
[references/fingerprints.md](references/fingerprints.md). Collect
|
|
62
|
+
everything about the domain that is not the brand name: registration facts,
|
|
63
|
+
DNS and mail records, certificate history, analytics and pixel IDs, the
|
|
64
|
+
block-page or error-page template, support-address pattern, hard-coded
|
|
65
|
+
partner hosts, and naming scheme. Record each with where it came from.
|
|
66
|
+
Fingerprint the seed fully before pivoting; a half-collected profile makes
|
|
67
|
+
later pivots look independent when they are not.
|
|
68
|
+
|
|
69
|
+
### 2 — Pivot
|
|
70
|
+
|
|
71
|
+
[references/pivots.md](references/pivots.md). For each fingerprint, query the
|
|
72
|
+
source class that can answer "who else has this": page-source indexes for
|
|
73
|
+
analytics IDs and templates, passive DNS for nameservers and mail records,
|
|
74
|
+
certificate transparency for subdomains and naming schemes, reverse WHOIS for
|
|
75
|
+
registration batches. Ordinary web search does not index page source; do not
|
|
76
|
+
treat an empty web-search result as a negative. Filter noisy pivots
|
|
77
|
+
(shared CDN nameservers, common mail includes) by requiring a second
|
|
78
|
+
fingerprint before a hit becomes a candidate.
|
|
79
|
+
|
|
80
|
+
### 3 — Expand names
|
|
81
|
+
|
|
82
|
+
Brands that rotate mirrors follow a scheme: numbered suffixes, hyphenated
|
|
83
|
+
digits, double-dash prefixes, country TLDs. Enumerate the scheme for every
|
|
84
|
+
brand found and keep only the variants that share a step-1 fingerprint.
|
|
85
|
+
A name match alone is a candidate, never a member.
|
|
86
|
+
|
|
87
|
+
### 4 — Entity
|
|
88
|
+
|
|
89
|
+
[references/entities.md](references/entities.md). Once a brand name exists,
|
|
90
|
+
collect the legal entity, licence number, address, and affiliate program from
|
|
91
|
+
the site's own terms page and from review aggregators, then reverse each of
|
|
92
|
+
those: licence registers list every domain under one licence, affiliate
|
|
93
|
+
programs list every brand they pay for, entity names appear in other sites'
|
|
94
|
+
terms pages. Keep conflicting operator names side by side with dates; operators
|
|
95
|
+
migrate and the conflict is often the history.
|
|
96
|
+
|
|
97
|
+
### 5 — Score
|
|
98
|
+
|
|
99
|
+
[references/scoring.md](references/scoring.md). Weight each link by how hard
|
|
100
|
+
it is to share by accident. One analytics account or one custom template is
|
|
101
|
+
very strong; one registrar or one CDN nameserver pair is medium. Two signals
|
|
102
|
+
from the same vendor account count once. Label each domain Confirmed,
|
|
103
|
+
Probable, Possible, or Unrelated, and keep the reasoning visible in the
|
|
104
|
+
evidence matrix.
|
|
105
|
+
|
|
106
|
+
### 6 — Loop
|
|
107
|
+
|
|
108
|
+
Every Confirmed or Probable domain becomes a new seed and goes through steps 1
|
|
109
|
+
to 5. Possible domains are parked with the fingerprint that would promote them.
|
|
110
|
+
Stop when a complete pass finds nothing new, or when the user's question is
|
|
111
|
+
answered and further expansion would only add mirrors.
|
|
112
|
+
|
|
113
|
+
### 7 — Report
|
|
114
|
+
|
|
115
|
+
[references/report-template.md](references/report-template.md). Terms:
|
|
116
|
+
[references/terminology.md](references/terminology.md).
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
reports/similar-domain/<seed>_cluster.md
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Lead with the verdict, then the cluster table sorted Confirmed → Probable →
|
|
123
|
+
Possible, the evidence matrix (domain × fingerprint), the entity chain with
|
|
124
|
+
dates, outliers that were checked and rejected, coverage caveats, and the
|
|
125
|
+
pivot log. Grep banned strings after writing.
|
|
126
|
+
|
|
127
|
+
## Progress
|
|
128
|
+
|
|
129
|
+
Track a task plan. One item is `in_progress` at a time.
|
|
130
|
+
|
|
131
|
+
1. Pass pre-flight
|
|
132
|
+
2. Fingerprint every seed
|
|
133
|
+
3. Run and log pivots
|
|
134
|
+
4. Expand naming schemes
|
|
135
|
+
5. Collect entity chain
|
|
136
|
+
6. Score and label
|
|
137
|
+
7. Loop until no new domain
|
|
138
|
+
8. Write report and proofread
|
|
139
|
+
|
|
140
|
+
## Boundary
|
|
141
|
+
|
|
142
|
+
| Need | Skill |
|
|
143
|
+
|---|---|
|
|
144
|
+
| Related domains, mirrors, sister sites, who runs a website, merchant-site background | `similar-domain` |
|
|
145
|
+
| Anything inside a transaction file: upstream streams behind a descriptor, wallet currency, preset menus, frozen-rate batches, retail constants, currency-amount ladders | `tld-detection` (`scripts/detect_streams.py`, then the ladder run) |
|
|
146
|
+
| Supervised fraud labels, holdout precision/recall, rule packages | `fraud-analysis` |
|
|
147
|
+
| Volume, journey, auth rate, decline mix | `payment-analysis` |
|
|
148
|
+
|
|
149
|
+
A domain cluster from this skill can feed a merchant-risk decision; it is not
|
|
150
|
+
itself a fraud finding. When the user also has authorization data for the
|
|
151
|
+
merchant, hand that part to `tld-detection`: this skill stops at the websites,
|
|
152
|
+
mirrors and operators.
|
|
153
|
+
|
|
154
|
+
## Reference index
|
|
155
|
+
|
|
156
|
+
| Need | Read |
|
|
157
|
+
|---|---|
|
|
158
|
+
| Inputs, limits, order that must not move | [workflow.md](references/workflow.md) |
|
|
159
|
+
| What to collect per domain and why it identifies an operator | [fingerprints.md](references/fingerprints.md) |
|
|
160
|
+
| Which source class answers each "who else" question, and the traps | [pivots.md](references/pivots.md) |
|
|
161
|
+
| Operator, licence, affiliate, terms-page pivots | [entities.md](references/entities.md) |
|
|
162
|
+
| Link weights, independence, labels | [scoring.md](references/scoring.md) |
|
|
163
|
+
| Report chapters | [report-template.md](references/report-template.md) |
|
|
164
|
+
| Language and banned strings | [terminology.md](references/terminology.md) |
|
|
165
|
+
|
|
166
|
+
## Sanity check
|
|
167
|
+
|
|
168
|
+
- [ ] Seeds, question, and access limits are stated; blocked sites are still fingerprinted
|
|
169
|
+
- [ ] Every fingerprint has a source and a timestamp; local resolver artefacts are excluded
|
|
170
|
+
- [ ] Every pivot is logged with source class, query, and hit count, including empty ones
|
|
171
|
+
- [ ] Noisy pivots (CDN nameservers, common mail includes) never add a domain on their own
|
|
172
|
+
- [ ] Name-scheme candidates are kept only when they share a fingerprint
|
|
173
|
+
- [ ] Entity facts cite the page they came from; conflicting operator names are dated, not merged
|
|
174
|
+
- [ ] Two signals from one vendor account are counted once
|
|
175
|
+
- [ ] Every cluster member has a label and the matrix shows why
|
|
176
|
+
- [ ] Loop ran until a full pass added nothing, or the stop reason is stated
|
|
177
|
+
- [ ] Verdict is ≤3 sentences; no "owned by" without an entity citation
|
|
178
|
+
- [ ] Outliers checked and rejected are listed with the reason
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Entities
|
|
2
|
+
|
|
3
|
+
Step 4 names and dates the cluster. It runs after at least one brand name
|
|
4
|
+
is known and after the technical links exist; it explains the cluster, it
|
|
5
|
+
does not replace the evidence.
|
|
6
|
+
|
|
7
|
+
## What to collect per brand
|
|
8
|
+
|
|
9
|
+
| Fact | Where it usually is |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Legal operator name and company number | Site footer, terms and conditions, "about" page, review aggregators |
|
|
12
|
+
| Licence authority and number | Footer seal, validator link, regulator register |
|
|
13
|
+
| Registered address | Terms page, regulator register |
|
|
14
|
+
| Launch year and relaunches | Review aggregators, archives, certificate history |
|
|
15
|
+
| Affiliate program | Footer "Affiliates" link, affiliate directories |
|
|
16
|
+
| Platform vendor | Footer, script hosts, review sites |
|
|
17
|
+
| Previous operators | Older reviews, forum threads, archive snapshots |
|
|
18
|
+
| Complaint patterns | Review aggregators, complaint forums |
|
|
19
|
+
|
|
20
|
+
Record each fact with its page and the date the page was written or
|
|
21
|
+
captured. Review aggregators disagree with each other because operators
|
|
22
|
+
migrate; keep every version with its date rather than picking one.
|
|
23
|
+
|
|
24
|
+
## Entity pivots
|
|
25
|
+
|
|
26
|
+
| Have | Reverse into | Yields |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Operator name | Aggregator "owner" pages, licence register, web search of terms-page text | Brand list under that operator |
|
|
29
|
+
| Licence number | Regulator register | Every licensed domain, including ones with no technical link yet |
|
|
30
|
+
| Affiliate program | Affiliate directories and forums | Brands marketed together |
|
|
31
|
+
| Address | Web search | Other entities at the address; treat corporate-service addresses as noise |
|
|
32
|
+
| Platform vendor | Vendor's client list, review sites | Vendor-level neighbours; filter, do not add |
|
|
33
|
+
| Previous operator | Same as operator name | The migration path (for example Curaçao entity → transitional entity → Anjouan entity) |
|
|
34
|
+
|
|
35
|
+
## Migration patterns worth expecting
|
|
36
|
+
|
|
37
|
+
Offshore operators reorganise often. Common shapes:
|
|
38
|
+
|
|
39
|
+
- **Entity swap, same platform** — the brand keeps its template and
|
|
40
|
+
tokens; only the footer entity and licence change. Technical evidence
|
|
41
|
+
stays continuous; corporate evidence shows a break.
|
|
42
|
+
- **Brand acquisition** — an old, once-reputable domain is bought and
|
|
43
|
+
relaunched on a new operator's platform. Registration date is old;
|
|
44
|
+
everything else matches the new cluster. Weight the technical evidence.
|
|
45
|
+
- **Mirror farms** — numbered or hyphenated domains registered in batches,
|
|
46
|
+
each redirecting or serving the parent brand. They inherit every
|
|
47
|
+
fingerprint and are usually Confirmed on the first pass.
|
|
48
|
+
- **White-label tenants** — several unrelated operators on one vendor
|
|
49
|
+
platform share vendor-level fingerprints but not operator-level ones.
|
|
50
|
+
This is the main source of false clusters.
|
|
51
|
+
|
|
52
|
+
## Adding a domain from entity evidence alone
|
|
53
|
+
|
|
54
|
+
Allowed only when the source is authoritative for domain membership: a
|
|
55
|
+
regulator's licence register, or the operator's own terms page listing its
|
|
56
|
+
domains. Label such domains Probable until a technical fingerprint
|
|
57
|
+
confirms them. Aggregator sister-site lists are hypotheses to test, not
|
|
58
|
+
grounds for membership.
|