From 5024e3ff42b069a31e5794e381f5e17fd265a3dd Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 4 Oct 2026 00:28:16 +0200 Subject: [PATCH] docs(14-05): complete sm-feedback-plugin plan --- .../14-05-SUMMARY.md | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 .planning/phases/14-domain-jobs-and-external-integrations/14-05-SUMMARY.md diff --git a/.planning/phases/14-domain-jobs-and-external-integrations/14-05-SUMMARY.md b/.planning/phases/14-domain-jobs-and-external-integrations/14-05-SUMMARY.md new file mode 100644 index 0000000..cd7f826 --- /dev/null +++ b/.planning/phases/14-domain-jobs-and-external-integrations/14-05-SUMMARY.md @@ -0,0 +1,304 @@ +--- +phase: 14-domain-jobs-and-external-integrations +plan: 05 +subsystem: feedback +tags: [sm-feedback-plugin, golem15.feedback, feedback-widget, g15office, cors, preflight, river, fetchguard, cabana, parity, upstream-sidecar, submodule] + +requires: + - phase: 14-domain-jobs-and-external-integrations + provides: "14-01 fetchguard.Client (AllowHostsMode, PostMultipart, WithTransport), tide upstream sidecars and summer parity:upstream; 14-04 the shared-plugin submodule workflow" + - phase: 07-user-plugin + provides: "golem15.user jwt.auth, GetApiArrayEvent and the user payload's feedback_widget_hidden default" +provides: + - "New shared core plugin repo git.golem15.com/golem15/sm-feedback-plugin (master pushed, b4b4135..af5d77c), mounted in fonoteka.go at plugins/golem15/feedback: plugin golem15.feedback, package feedback, requires golem15.user" + - "Widget API: GET {key}/config and POST {key}/submit behind a constant-time key check and a fail-closed Origin allow-list, buckets feedback-config (60/min) and feedback-submit (10/min); JWT PUT me/hidden; feedback_widget_hidden on every golem15.user payload" + - "Tables golem15_feedback_submissions and golem15_feedback_user_preferences (PHP's columns, strings as TEXT) and the typed singleton golem15_feedback_settings with a generated widget key" + - "G15Office River job (kind golem15.feedback.sync_g15office, queue feedback, 3 attempts) on a fetchguard client limited to the configured host, replaying PHP's recorded exchange" + - "Settings screen (code feedback), read-only submissions list, golem15.feedback.manage_settings, embed.js byte for byte at /plugins/golem15/feedback/assets/js/embed.js, feedback:import-settings" + - "surf (framework): OPTIONS on a CORS path answers with Laravel HandleCors headers (summercms.go b5d20b3)" + - "Parity: feedback section (feedbackRouteIDs, 4 routes, 28 PHP-recorded cases), job sidecar fixtures/jobs/feedback-g15office; corpus 175 routes, 172 ported, 3 pending (D-09)" +affects: [14-06 unit tests and gate (API-08 marked there), 15 cutover (feedback:import-settings, SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__*)] + +actuals: + tokens: 98000 # chars/4 over the added lines of all three repos (embed.js included) + tasks: 3 + commits: 1 # MEASURED in summercms.go: plan_head_before..plan_head_after (code only) + app_repo_commits: 7 # MEASURED in fonoteka.go: app_repo_head_before..app_repo_head_after + plugin_repo_commits: 3 # MEASURED in sm-feedback-plugin: the whole new repo (pushed) +plan_head_before: 7eb0174612855067a156d34baf4dcb31af408b05 +plan_head_after: b5d20b3bfdd712429e76e17cbb22e85db427e051 +app_repo_head_before: eea0b1607ef9b5d14b3a5186bf322e6eb45a4b9b +app_repo_head_after: 9fc38d9 +plugin_repo_head_after: af5d77c + +tech-stack: + added: [] + patterns: + - "A shared plugin's G15Office/vendor sidecar is replayed from the plugin's own testdata copy; the application keeps the recorded original under fixtures/jobs with a rows golden and a test that the two copies are byte-identical" + - "Origin-gated routes record the Origin and Access-Control-Request-* request headers (capture-rules keep them)" + - "A spec's query string goes in query:, never in path:" + - "Recordings of routes that queue jobs run PHP with QUEUE_CONNECTION=database; the job itself is recorded separately under sync with the upstream proxy" + +key-files: + created: + - ../fonoteka.go/plugins/golem15/feedback/ (whole repo: plugin.go, routes.go, admin.go, jobs.go, assets.go, assets/js/embed.js, README.md, config, lang/{en,pl}, models, updates, controllers/{api,submissions}, classes, console, internal/pgtest) + - ../fonoteka.go/parity/feedback_seed_test.go + - ../fonoteka.go/parity/fixtures/routes/{GET,POST,OPTIONS,PUT}___feedback_api_v1_* (28 fixtures) + - ../fonoteka.go/parity/fixtures/jobs/feedback-g15office.{upstream.yaml,rows.json} + - ../fonoteka.go/parity/upstream/scripts/g15office-task.yaml + modified: + - modules/surf/cors.go + - modules/surf/cors_coverage_test.go + - modules/surf/README.md + - docs/services/routing.md + - ../fonoteka.go/{.gitmodules,go.work,go.mod,summer.yaml,plugins.gen.go,app/app.go,README.md} + - ../fonoteka.go/parity/{check_corpus.go,routes.snapshot,manifest.yaml,parity_test.go,parity_contract_test.go,schema_diff_test.go,migrate_test.go,rollback_isolation_full_test.go,fonoteka_seed_test.go,fonoteka_reset.php,php_parity.sh,capture-rules.yaml,README.md} + +key-decisions: + - "surf answers OPTIONS on CORS paths with Laravel HandleCors headers: Cache-Control no-cache, private always; on a preflight the requested method (upper-cased) and headers echoed when * allows any, Vary, and PHP's default Content-Type text/html; charset=UTF-8" + - "The settings screen's code is feedback, not WinterCMS's settings: cabana keys settings screens by code across all plugins" + - "The G15Office job is dispatched through conga with a summer_jobs row labelled Golem15\\Feedback\\Jobs\\SyncFeedbackToG15Office and completed on success (PHP used a plain Laravel queue job)" + - "The G15Office client runs in fetchguard AllowHostsMode for the base URL's host, so it refuses an http base URL (PHP's curl would send over http)" + - "Submission string columns are TEXT: PHP's user_agent column is 255 characters while its validation allows 500" + - "The submissions list is read-only and has no form, so models/submission/fields.yaml was not created" + - "API-08 is not marked complete: 14-06 also declares it (as 14-04 did with INTG-02)" + +patterns-established: + - "feedback, feedback-closed and feedback-off are additive seed extras on both sides; without one the reset removes the feedback settings and the parity users' preferences" + +requirements-completed: [] + +coverage: + - id: D1 + description: "sm-feedback-plugin exists, is pushed and mounted; golem15.feedback loads after golem15.user and before golem15.fonoteka" + requirement: API-08 + verification: + - kind: unit + ref: "plugins/golem15/feedback#TestFeedbackPluginBoot; parity#TestMigrate* (activateAppPlugins order)" + status: pass + - kind: other + ref: "git -C plugins/golem15/feedback status --porcelain --branch → '## master...origin/master'; .gitmodules, go.mod, summer.yaml, plugins.gen.go" + status: pass + human_judgment: false + - id: D2 + description: "Widget config with the constant-time key check and the fail-closed Origin gate" + requirement: API-08 + verification: + - kind: unit + ref: "plugins/golem15/feedback#TestFeedbackConfig, TestKeyMatches, TestURLHost, TestAllowedOriginHosts" + status: pass + - kind: parity + ref: "parity#TestParityCorpus/GET___feedback_api_v1_{key}_config_feedback (10 cases)" + status: pass + human_judgment: false + - id: D3 + description: "Submit with PHP's validation, the image guard, the public screenshot and the job in one transaction; me/hidden; feedback_widget_hidden; the preflight" + requirement: API-08 + verification: + - kind: unit + ref: "plugins/golem15/feedback#TestFeedbackSubmit, TestMeHidden, TestFeedbackApiArrayHook, TestFeedbackOptionsPreflight; classes#TestImageGuardCopy; summercms modules/surf#TestCORSOptionsMatchesLaravel" + status: pass + - kind: parity + ref: "parity#TestParityCorpus/POST___feedback_api_v1_{key}_submit_feedback (9), OPTIONS___feedback_api_v1_{any}_feedback (2), PUT___feedback_api_v1_me_hidden_jwt (7, two with the user fetch); coverage 172 ported, 3 pending" + status: pass + human_judgment: false + - id: D4 + description: "G15Office job matches PHP's recorded requests; failures stored and retried; nothing sent unconfigured" + requirement: API-08 + verification: + - kind: unit + ref: "plugins/golem15/feedback/classes#TestSyncG15Office (php-recorded sidecar + 9 failure paths), TestTaskText" + status: pass + - kind: other + ref: "parity#TestFeedbackJobSidecarMatchesPlugin, TestUpstreamSidecarsAreReplayed" + status: pass + human_judgment: false + - id: D5 + description: "Settings screen, submissions list, embed.js, settings import" + requirement: API-08 + verification: + - kind: unit + ref: "plugins/golem15/feedback#TestFeedbackAdminSchemas, TestEmbedJSServed, TestFeedbackImportSettings" + status: pass + human_judgment: false + +duration: 100min +completed: 2026-10-04 +status: complete +--- + +# Phase 14 Plan 05: sm-feedback-plugin Summary + +**A new shared plugin, sm-feedback-plugin, ports the WinterCMS feedback widget to Go. The embedded widget loads its configuration, a visitor sends a report with a screenshot, and a signed-in collector hides the widget, all as PHP answers, behind the same key and Origin gate. Each report reaches G15Office as a task with its screenshot, in the requests PHP sent. Operators configure the widget and read the reports in the admin.** + +## Performance + +- **Duration:** about 100 min +- **Started:** 2026-10-03T21:44:47Z +- **Completed:** 2026-10-04T00:25Z +- **Tasks:** 3 +- **Commits:** 3 in sm-feedback-plugin (pushed), 7 in fonoteka.go, 1 code commit in summercms.go (plus this summary) +- **Files:** 37 in the plugin (+5430), 52 in fonoteka.go (+1178/-27), 4 in summercms.go (+106/-2) + +## Accomplishments + +- **sm-feedback-plugin (D-12).** The empty remote was cloned at `plugins/golem15/feedback`, built, committed and pushed, then registered with `git submodule add`. `master` is at `af5d77c` and in sync with origin. fonoteka.go loads `golem15.feedback` after `golem15.user` and before `golem15.fonoteka`: `go.work`, `go.mod` (require and replace), `summer.yaml`, the regenerated `plugins.gen.go` and `app.PluginIDs` all name it. +- **Widget config (D-13).** `GET /_feedback/api/v1/{key}/config` checks the key in constant time and answers 404 `Not found` when the widget is disabled, has no key or gets the wrong one. The Origin gate lets a request with no Origin through. A request whose Origin host, lower-cased, is not on the list gets 403 `Origin not allowed`, and an empty list refuses every Origin. `models.URLHost` follows PHP's `parse_url`, including `localhost:3000` and IPv6 brackets. `?lang=en` selects the English labels. +- **Submit (D-13).** + - `POST {key}/submit` validates with PHP's rules and returns the 422 envelope with Laravel's messages in rule order. + - The screenshot must pass a copy of ImageContentGuard, or the answer is 422 `The file is not a valid image.`. + - One transaction stores the submission, queues the G15Office job and stores the public screenshot (attachOne `screenshot`). A failed write keeps no row and no job. The answer is 202 `{"success":true}`. +- **Hiding the widget.** JWT `PUT me/hidden` accepts Laravel booleans, writes only the caller's row and answers `{"hidden":bool}`. A `GetApiArrayEvent` listener puts `feedback_widget_hidden` on every golem15.user payload. The recorded me/hidden cases include a following `GET /_user/api/v1/fetch`, so the payload key is asserted against PHP. +- **Preflight.** surf's CORS layer answers `OPTIONS` on `_feedback/api/*`, and no route is registered for it. surf now sends the headers PHP sends (see Deviation 1). +- **G15Office (D-13, D-15).** + - `classes.SyncG15Office` builds PHP's title. `Str::limit` is ported with `mb_strwidth` widths, using a table generated from PHP. The whitespace collapse is PCRE `\s`, so it includes VT. + - It builds PHP's Markdown description, the type map and the settings' status and priority. + - It posts the task JSON, then the screenshot as the multipart part `file`. Both requests go through `fetchguard` in AllowHostsMode for the base URL's host, with `Accept: application/json`, the Bearer token and no User-Agent. + - Success stores `sent` and the task id. Failure stores `failed` and PHP's message, and returns the error so River retries, up to `conga.MaxAttempts(3)`. When unconfigured, nothing is sent. + - PHP ran the job under the sync queue through the recording proxy. Its exchange is replayed offline. A one-word mutation of the title limit failed the replay. +- **Admin and assets.** + - The settings screen (code `feedback`) uses cabana's field types only. The colors are text fields, the widget key is read-only through `attributes`, and the defaults are `initSettingsData`. A row saved without a key gets a new one. + - The submissions list is read-only: no form and no buttons, every column plain text. + - The plugin registers the `golem15.feedback.manage_settings` permission (developer role) and a Feedback menu. + - `embed.js` is served byte for byte, with an ETag. + - `feedback:import-settings` copies the WinterCMS row once, the widget key included. Without `--force` it refuses settings that were already changed. +- **Parity.** + - 28 cases were recorded against PHP: 10 config, 9 submit, 2 OPTIONS and 7 me/hidden. + - The submit cases include each limit at the boundary and one past it (5000-character multibyte message, page_url, user_agent, console_log), the rule-order 422, a non-image, a spoofed JPEG, a foreign Origin and a wrong key. + - The config cases include an uppercase Origin, `localhost:3000`, `null`, an empty list, a disabled widget and an unconfigured widget. + - The corpus has 175 routes: 172 ported and passing, and 3 pending (D-09). `check_corpus --require-recorded --check-secrets` is green. + +## Task Commits + +sm-feedback-plugin (`git@git.golem15.com:golem15/sm-feedback-plugin.git`, master, pushed): +1. `b4b4135` feat: golem15.feedback plugin with the widget config route (Task 1) +2. `92e1b4e` feat: feedback submissions with screenshots, the hide-widget preference and its user payload key (Task 2) +3. `af5d77c` feat: G15Office job, widget settings and submissions admin, embed.js and the settings importer (Task 3) + +fonoteka.go (not pushed): +1. `a29c809` chore(14-05): mount sm-feedback-plugin at plugins/golem15/feedback (`.gitmodules` + gitlink only) +2. `a845178` feat(14-05): the application loads golem15.feedback after golem15.user (Task 1) +3. `2028880` feat(14-05): the embedded widget loads its configuration from golem15.feedback exactly as from PHP (Task 1) +4. `e497e94` chore(14-05): bump sm-feedback-plugin (gitlink only, Task 2) +5. `8d5d451` feat(14-05): a visitor submits feedback, a collector hides the widget and the user payload reports it, as in PHP (Task 2) +6. `86afa59` chore(14-05): bump sm-feedback-plugin (gitlink only, Task 3) +7. `9fc38d9` feat(14-05): the team receives each submission as a G15Office task with its screenshot, as PHP sent it (Task 3) + +summercms.go: +- `b5d20b3` fix(14-05): surf answers OPTIONS on CORS paths with Laravel's HandleCors headers (README and routing docs updated; TestDocsTree and docs:build --check pass) + +Tracer gate: after Task 1 the `` was re-run end to end (vet, the named tests with `-race`, the corpus, check_corpus, submodule status) and passed. The expansion tasks then went ahead. + +## Deviations from Plan + +### Auto-fixed issues + +**1. [Rule 1 - Bug, framework] surf's OPTIONS answer did not match PHP** +- **Found during:** Task 1 recording +- **Issue:** PHP's preflight answers 204 with `Access-Control-Allow-Methods: POST` and `Access-Control-Allow-Headers: content-type` (php-cors echoes the request when `*` allows any), `Cache-Control: no-cache, private` and `Content-Type: text/html; charset=UTF-8`. A plain OPTIONS answers 204 with the Cache-Control. surf sent `*` for both Allow headers and no Cache-Control or Content-Type, so the plan's assumption that "the recorded preflight cases must replay green" failed. +- **Fix:** `surf.writeOptions` sends what HandleCors sends. A configured method or header list is still sent as configured. Added `TestCORSOptionsMatchesLaravel` and updated the surf README and `docs/services/routing.md`. +- **Commit:** b5d20b3 (summercms.go) + +**2. [Rule 3 - Blocking] The Origin gate could not be replayed** +- **Issue:** `capture-rules.yaml` keeps only Authorization, Content-Type, Accept and Accept-Language, so a recorded Origin would be dropped and a 403 case would replay as 200. +- **Fix:** The rules also keep `Origin`, `Access-Control-Request-Method` and `Access-Control-Request-Headers`. Existing fixtures are unaffected. +- **Commit:** 2028880 + +**3. [Rule 2 - Privacy] The PHP checkout's `.env` could point the job at a real G15Office** +- **Fix:** `php_parity.sh` exports `G15_OFFICE_BASE_URL`, `G15_OFFICE_TOKEN` and `G15_OFFICE_PROJECT` empty unless the caller sets them. The job recording sets them to `https://office.parity.test` and a fake token, behind the scripted proxy. +- **Commit:** 2028880 + +**4. [Rule 1 - Bug] The fonoteka seed left alice's user flags to chance** +- **Issue:** The me/hidden user-payload step expected `has_self_set_password: true`. The shared replay database left the flag at whatever an earlier user-api replay had set. +- **Fix:** `seedFonotekaCase` now sets alice's `has_self_set_password`, `must_change_password`, `marketing_consent` and `is_onboarded` to PHP's values. No other fonoteka-seeded fixture shows them. +- **Commit:** 8d5d451 + +**5. [Rule 3 - Blocking] `TestParityContract` rejected the new ported routes** +- **Fix:** Added the `feedbackSeedRoutes` set. +- **Commit:** 2028880 + +### Plan details refined + +**6.** All 28 route cases were recorded in one session and committed with Task 1. The submit, OPTIONS and me/hidden routes stayed `pending` until Task 2 flipped them. The routes were recorded with `QUEUE_CONNECTION=database`: under `sync`, PHP runs the job inside the request, and an unconfigured G15Office would make the submit a 500. + +**7.** The settings screen's code is `feedback`, not WinterCMS's `settings`. cabana keys settings screens by code across all plugins, so a generic code would clash with the next shared plugin. + +**8.** The job goes through `conga.Dispatch`, with a `summer_jobs` row labelled with the PHP job class, and is completed on success. Inside the one transaction it is queued before the screenshot is written, so `TestFeedbackSubmit/rolled-back-write-queues-nothing` proves that a failed screenshot write leaves no job behind. The order is not visible outside the transaction. + +**9.** No `models/submission/fields.yaml` was created. The list is read-only and has no form, which `TestFeedbackAdminSchemas` asserts. + +**10.** `TestImageGuardCopy` compares the copy with the framework guard (`attach.IsAllowedImage`) and with PHP's verdicts on the recorded fixtures. A plugin in its own repository cannot import the application's fonoteka guard. + +**11.** The plugin replays the G15Office sidecar from its own `classes/testdata` copy. The application keeps the recorded original under `fixtures/jobs`, with a rows golden (required by `TestUpstreamSidecarsAreReplayed`), and `TestFeedbackJobSidecarMatchesPlugin` keeps the two copies byte-identical. + +**12.** The G15Office client uses AllowHostsMode, so it accepts an https base URL only. PHP's curl would also post over http. + +**13.** All string columns are TEXT. PHP's `user_agent` is `string(255)`, but its own rule allows 500 characters, which Postgres would refuse. + +**14.** The submit route has nine cases, because the Go replay keeps one app per route and the bucket allows ten. `TestFeedbackSubmit/throttle` asserts that the 11th post gets a 429. + +**15.** Extra tests beyond the plan: `TestKeyMatches`, `TestURLHost` (PHP `parse_url` vectors), `TestAllowedOriginHosts` and `TestTaskText` (Str::limit and buildTitle/buildDescription against PHP outputs). + +**16.** The fonoteka.go README layout table now lists the golem and feedback submodules. The golem row was missing since 14-04. + +**17.** Commits land on `master` in all three repos, as in the earlier Phase 14 plans (`branching_strategy: none`, sequential executor). gsd-tools reports `master` as protected and `allow_default_branch_commits` is unset; the orchestrator's sequential-execution instructions were followed. + +--- + +**Total deviations:** 17. Five are auto-fixes and twelve are refinements. **Impact:** the only framework change is surf's OPTIONS answer. It is documented and limited to OPTIONS requests on CORS paths. There is no scope creep. + +## Issues Encountered + +- `/tmp` hit its disk quota. The parity root moved to `~/.cache/summercms-parity/p1405`, and Go and PHP temporary files to `~/.cache/gotest-tmp`. Older caches in `/tmp` left by earlier sessions were not touched. +- A spec with `?lang=en` inside `path:` was sent with an escaped path and got Winter's 404 page. It was re-recorded with `query:`. +- `artisan serve` keeps a child `php -S` alive after the parent is killed. The child was stopped by its PID. +- The submit recordings wrote two screenshots into the PHP checkout's `storage/app/uploads/public/6ac`, and that directory was removed. `git status` in the PHP checkout shows only the user's own pre-existing files. + +## Verification + +- sm-feedback-plugin: `go vet ./...` and `go test ./...` pass both standalone (`GOWORK=off`) and in the workspace. Every named test passes with `-race`. +- fonoteka.go: `go vet ./...` passes, `go test ./... -count=1` passes (the app and parity packages), and `go test ./... -short` passes. The fonoteka, golem and user plugin suites pass with `-count=1`. +- Parity: `TestParityCorpus` reports `recorded 175/175 passing 172 failing 0 unrecorded 0 pending 3`. `TestCheckCorpusPortedCaseStatus`, `TestParityContract`, `TestSchemaMatchesPHPSnapshot`, `TestUpstreamSidecarsAreReplayed` and `TestFeedbackJobSidecarMatchesPlugin` pass. `check_corpus --routes … --require-recorded --check-secrets` passes with 175/175. +- summercms.go: `go vet ./...` and `go test ./... -count=1` pass. `TestDocsTree` and `summer docs:build --check` pass. +- Acceptance greps for all three tasks pass: + - `.gitmodules` has 1 feedback path, and the plugin's `origin/master` resolves. + - `feedbackRouteIDs` appears 3 times, and `routes.snapshot` has 4 `_feedback/api/v1` lines. + - `expectedRouteCount` and `expectedPHPRoutes` are 175, and `expectedPortedRoutes` is 172. + - `ConstantTimeCompare` appears once, and `feedback_widget_hidden` is in plugin.go. + - `routes.go` has no `Options(`. + - The embed.js sha256 values are identical, and fields.yaml has no `colorpicker`. + - The job sidecar's Authorization is `Bearer {{secret:g15office-token}}`, and jobs.go has `conga.MaxAttempts(3)`. + +## Known Stubs + +None. + +## Threat Flags + +None. The new surfaces are in the plan's threat register: +- The public widget routes (T-14-30, T-14-31), with the constant-time key check, the fail-closed Origin list and the per-IP buckets. +- The screenshot (T-14-32), with the rules and the sniff-and-decode guard. +- The G15Office token and forwarding (T-14-33, T-14-34). The token comes from config only, the sidecars mask it, the client is limited to the configured host, and the job logs only the submission id. +- me/hidden (T-14-35), which writes only the principal's row. +- The admin list (T-14-36), which renders text columns only. + +The `embed.js` route serves an embedded static file and reads no input. No key, token or DSN literal was pushed: the diff was scanned before each push, and the tests use only short fake secrets. + +## User Setup Required + +None for development. At cutover (Phase 15) the operator: +- sets `SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL` (https), `__TOKEN` and `__PROJECT`; +- runs `feedback:import-settings` once, so embed snippets keep their widget key; +- runs a queue worker that serves the `feedback` queue. + +## Next Phase Readiness + +- 14-06 picks up the unit-test gate. It should add `./plugins/golem15/feedback/...` to `check-phase14.sh`'s plugin list and mark API-08 (left Pending here because 14-06 also declares it). + +--- +*Phase: 14-domain-jobs-and-external-integrations* +*Completed: 2026-10-04* + +## Self-Check: PASSED + +Every listed file exists; commits b5d20b3 (summercms.go), a29c809, a845178, 2028880, e497e94, 8d5d451, 86afa59 and 9fc38d9 (fonoteka.go) and b4b4135, 92e1b4e and af5d77c (sm-feedback-plugin, on origin/master) are present. The final `go test ./... -count=1` in fonoteka.go passed after the last commit.